Skip to content

Commit 4a43e75

Browse files
committed
feat(typescript)!: speak graph schema 2.0.0 — TS-prefixed vocabulary, CanNode keys, fail-fast version check (closes #268)
Rewrite the read-only TS Neo4j backend to query the canonical 2.0.0 graph (CanNode identity, TS-prefixed labels/relationships) and reject any other schema on connect. - Every Cypher query maps to the 2.0.0 vocabulary: :Symbol -> :CanNode, :Callable/:Class/... -> :TSCallable/:TSClass/..., CALLS/DECLARES/HAS_METHOD/ RESOLVES_TO/HAS_MODULE -> TS_CALLS/TS_DECLARES/TS_HAS_METHOD/TS_RESOLVES_TO/ TS_HAS_MODULE. Application matched by `id ENDS WITH "/" + $app` (no `name`); modules keyed by `_module` (no `file_key`). - Call sites are now :TSBodyNode {kind:"call"} reached via TS_HAS_BODY_NODE and resolved through TS_RESOLVES_TO (no :CallSite nodes); reconstruct reads the target from `callee`. - Fail-fast: `_check_schema_version` reads (:Application).schema_version once in __init__ and raises the new CldkSchemaMismatchException unless it is "2.0.0". - Accessors whose vocabulary 2.0.0 does not project raise NotImplementedError (no JSON fallback exists for a read-only Cypher client): get_decorators, get_class_decorators, get_methods_with_decorators, get_classes_with_decorators, get_all_fields, get_interface_properties, get_imports, get_all_exports, get_all_variables. Whole-object reconstruction degrades those sub-fields to empty so graph-supported accessors keep working. Public facade names and return types are unchanged. - Tests: new test_typescript_neo4j_schema.py covers the version gate and the fallback raises without a live graph; the stubbed-graph parity tests adopt the 2.0.0 query strings; the live-graph integration suite is skipped until the analyzer pin moves to >=1.0.0 (Task 9).
1 parent 74510fb commit 4a43e75

6 files changed

Lines changed: 302 additions & 202 deletions

File tree

cldk/analysis/typescript/neo4j/neo4j_backend.py

Lines changed: 161 additions & 190 deletions
Large diffs are not rendered by default.

cldk/analysis/typescript/neo4j/reconstruct.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,8 @@ def callsite(props: Props) -> TSCallsite:
106106
argument_types=list(props.get("argument_types", []) or []),
107107
type_arguments=list(props.get("type_arguments", []) or []),
108108
return_type=props.get("return_type"),
109-
callee_signature=props.get("callee_signature"),
109+
# schema 2.0.0 call-site body nodes key the resolved target as ``callee``.
110+
callee_signature=props.get("callee_signature", props.get("callee")),
110111
is_constructor_call=props.get("is_constructor_call", False),
111112
is_optional_chain=props.get("is_optional_chain", False),
112113
start_line=props.get("start_line", -1),
@@ -370,8 +371,9 @@ def namespace(
370371

371372
def module(props: Props, **children: Any) -> TSModule:
372373
return TSModule(
373-
file_path=props.get("file_key", props.get("file_path", "")),
374-
module_name=props.get("module_name", ""),
374+
# schema 2.0.0 modules carry the project-relative path as ``_module``.
375+
file_path=props.get("_module", props.get("file_path", "")),
376+
module_name=props.get("module_name", props.get("name", "")),
375377
is_tsx=props.get("is_tsx", False),
376378
is_declaration_file=props.get("is_declaration_file", False),
377379
content_hash=props.get("content_hash"),

cldk/utils/exceptions/exceptions.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ def __init__(self, message: str) -> None:
8787
super().__init__(self.message)
8888

8989

90+
class CldkSchemaMismatchException(Exception):
91+
"""Exception raised when a persisted graph's schema version is not the one this SDK speaks.
92+
93+
The Neo4j-backed TypeScript backend reads ``(:Application).schema_version`` on first use and
94+
raises this if it differs from the graph schema the SDK was written against. Re-analyze the
95+
project with a ``codeanalyzer-typescript`` whose Neo4j projection emits the expected schema.
96+
97+
Attributes:
98+
message (str): A descriptive error message naming the found and expected schema versions.
99+
100+
See Also:
101+
:class:`~cldk.analysis.typescript.neo4j.neo4j_backend.TSNeo4jBackend`: raises this on a
102+
version mismatch.
103+
"""
104+
105+
def __init__(self, message: str) -> None:
106+
"""Initialize the exception with a descriptive message.
107+
108+
Args:
109+
message: A descriptive error message naming the found vs. expected schema versions
110+
and how to resolve the mismatch.
111+
"""
112+
self.message = message
113+
super().__init__(self.message)
114+
115+
90116
class CodeanalyzerUsageException(Exception):
91117
"""Exception raised for incorrect CodeAnalyzer usage.
92118

tests/analysis/typescript/test_typescript_get_method_functions.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,15 +218,15 @@ def stub_neo4j_backend():
218218
baz_props = _callable_props(_baz())
219219
qux_props = _callable_props(_qux())
220220

221-
has_method_query = "MATCH (o:Symbol {signature: $sig})-[:HAS_METHOD]->(m:Callable {name: $name}) RETURN properties(m) AS p LIMIT 1"
221+
has_method_query = "MATCH (o:CanNode {signature: $sig})-[:TS_HAS_METHOD]->(m:TSCallable {name: $name}) RETURN properties(m) AS p LIMIT 1"
222222
exact_sig_query = (
223-
"MATCH (parent)-[:DECLARES]->(c:Callable {signature: $sig}) "
224-
"WHERE (parent:Module OR parent:Namespace) AND c._module IN $mods "
223+
"MATCH (parent)-[:TS_DECLARES]->(c:TSCallable {signature: $sig}) "
224+
"WHERE (parent:TSModule OR parent:TSNamespace) AND c._module IN $mods "
225225
"RETURN properties(c) AS p LIMIT 1"
226226
)
227227
short_name_query = (
228-
"MATCH (parent)-[:DECLARES]->(c:Callable {name: $name}) "
229-
"WHERE (parent:Module OR parent:Namespace) AND c._module IN $mods AND c.signature STARTS WITH $prefix "
228+
"MATCH (parent)-[:TS_DECLARES]->(c:TSCallable {name: $name}) "
229+
"WHERE (parent:TSModule OR parent:TSNamespace) AND c._module IN $mods AND c.signature STARTS WITH $prefix "
230230
"RETURN properties(c) AS p LIMIT 1"
231231
)
232232

tests/analysis/typescript/test_typescript_neo4j_backend.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,20 @@ def _neo4j_reachable() -> bool:
6868
return False
6969

7070

71-
pytestmark = pytest.mark.skipif(
72-
not _neo4j_reachable(),
73-
reason=f"no Neo4j reachable at {NEO4J_URI} (set CLDK_TEST_NEO4J_URI / _USER / _PASSWORD)",
74-
)
71+
# The read-only backend now speaks graph schema 2.0.0 (TS-prefixed vocabulary, CanNode keys), but
72+
# the pinned ``codeanalyzer-typescript`` is still 0.4.3, whose ``--emit neo4j`` projection predates
73+
# that schema — so this out-of-band-populated integration suite cannot produce a conformant graph
74+
# yet. It is unconditionally skipped until the analyzer pin moves to >=1.0.0 (release-train Task 9);
75+
# the ``_neo4j_reachable`` guard is retained for when it is re-enabled.
76+
pytestmark = [
77+
pytest.mark.skipif(
78+
not _neo4j_reachable(),
79+
reason=f"no Neo4j reachable at {NEO4J_URI} (set CLDK_TEST_NEO4J_URI / _USER / _PASSWORD)",
80+
),
81+
pytest.mark.skip(
82+
reason="needs a graph schema 2.0.0 database (codeanalyzer-typescript>=1.0.0); the pin is still 0.4.3 until Task 9 moves it",
83+
),
84+
]
7585

7686

7787
def _codeanalyzer_ts_exec() -> list[str]:
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
################################################################################
2+
# Copyright IBM Corporation 2026
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
################################################################################
16+
17+
"""Unit tests for the TS Neo4j backend's graph-schema contract (no live Neo4j required).
18+
19+
Two things are exercised here without ever opening a Bolt connection:
20+
21+
* the fail-fast ``schema_version`` gate (``_check_schema_version``) the backend runs on first use;
22+
* the accessors whose vocabulary is *not projected* into graph schema 2.0.0 (decorators,
23+
attributes/fields, module imports/exports, variables) — these must raise a clear
24+
``NotImplementedError`` rather than silently returning wrong data.
25+
26+
Both are tested against a bare ``__new__`` instance (no ``__init__``, so no driver), because the
27+
logic under test never needs a real session.
28+
"""
29+
30+
import pytest
31+
32+
from cldk.analysis.typescript.neo4j import TSNeo4jBackend
33+
from cldk.utils.exceptions.exceptions import CldkSchemaMismatchException
34+
35+
36+
def _bare_backend() -> TSNeo4jBackend:
37+
"""A backend instance with no live driver — enough to exercise pure query/guard logic."""
38+
return TSNeo4jBackend.__new__(TSNeo4jBackend)
39+
40+
41+
# -----[ schema-version gate ]-----
42+
def test_schema_version_mismatch_fails_fast():
43+
backend = _bare_backend()
44+
with pytest.raises(CldkSchemaMismatchException):
45+
backend._check_schema_version(expected="2.0.0", found="1.0.0")
46+
47+
48+
def test_schema_version_match_passes():
49+
backend = _bare_backend()
50+
# An exact match is a no-op (returns None, raises nothing).
51+
assert backend._check_schema_version(expected="2.0.0", found="2.0.0") is None
52+
53+
54+
def test_schema_version_queried_from_application_when_not_supplied(monkeypatch):
55+
backend = _bare_backend()
56+
monkeypatch.setattr(backend, "_run", lambda *a, **k: [{"v": "2.0.0"}])
57+
# Reads (:Application).schema_version and finds the supported version ⇒ passes.
58+
assert backend._check_schema_version(expected="2.0.0") is None
59+
60+
monkeypatch.setattr(backend, "_run", lambda *a, **k: [{"v": "1.0.0"}])
61+
with pytest.raises(CldkSchemaMismatchException):
62+
backend._check_schema_version(expected="2.0.0")
63+
64+
65+
def test_schema_version_absent_fails_fast(monkeypatch):
66+
backend = _bare_backend()
67+
# No Application row at all (empty/foreign DB) ⇒ found is None ⇒ mismatch.
68+
monkeypatch.setattr(backend, "_run", lambda *a, **k: [])
69+
with pytest.raises(CldkSchemaMismatchException):
70+
backend._check_schema_version(expected="2.0.0")
71+
72+
73+
# -----[ accessors with no graph support in schema 2.0.0 ]-----
74+
_FALLBACK_CALLS = [
75+
("get_decorators", ("src/x.f",)),
76+
("get_class_decorators", ("src/x.C",)),
77+
("get_methods_with_decorators", (["Get"],)),
78+
("get_classes_with_decorators", (["Controller"],)),
79+
("get_all_fields", ("src/x.C",)),
80+
("get_interface_properties", ("src/x.I",)),
81+
("get_imports", ()),
82+
("get_all_exports", ()),
83+
("get_all_variables", ()),
84+
]
85+
86+
87+
@pytest.mark.parametrize("method_name,args", _FALLBACK_CALLS)
88+
def test_unprojected_accessor_raises_not_implemented(method_name, args):
89+
backend = _bare_backend()
90+
with pytest.raises(NotImplementedError, match="graph schema 2.0.0"):
91+
getattr(backend, method_name)(*args)

0 commit comments

Comments
 (0)