4545* call *sites* are body nodes — ``(:TSBodyNode {kind: "call"})`` reached via
4646 ``TS_HAS_BODY_NODE`` and resolved through ``TS_RESOLVES_TO`` — not standalone
4747 call-site nodes;
48- * every project-owned node carries a ``_module`` provenance property (the
49- project-relative path), so a single database may hold several applications —
50- all queries here are scoped to this backend's application by the set of its
51- module ``_module`` keys.
48+ * module nodes carry a ``_module`` property (the project-relative path); this
49+ backend further *assumes* — to-verify against a live 1.0.0 graph, see the
50+ ``VERIFY(2.0.0-e2e)`` markers — that every project-owned node carries the same
51+ ``_module`` provenance property, so a single database may hold several
52+ applications and all queries here can be scoped to this backend's application
53+ by the set of its module ``_module`` keys.
5254
5355Parity caveats (inherent to what schema 2.0.0 projects, not bugs):
5456
9193 TSTypeAlias ,
9294 TSVariableDeclaration ,
9395)
94- from cldk .utils .exceptions .exceptions import CldkSchemaMismatchException , CodeanalyzerExecutionException
96+ from cldk .utils .exceptions .exceptions import CldkSchemaMismatchException , CodeanalyzerExecutionException , CodeanalyzerUsageException
9597
9698logger = logging .getLogger (__name__ )
9799
@@ -139,9 +141,16 @@ def __init__(
139141 self ._database = neo4j_database
140142 self ._driver = GraphDatabase .driver (neo4j_uri , auth = (neo4j_username , neo4j_password ))
141143
142- # Fail fast if the persisted graph isn't the schema this SDK speaks.
144+ # Fail fast if the persisted graph isn't the schema this SDK speaks. This runs *before*
145+ # the application-id resolution on purpose: a pre-2.0.0 graph has no `can://` ids, so
146+ # resolving first would report "no application found" instead of the (more actionable)
147+ # schema mismatch.
143148 self ._check_schema_version ()
144149
150+ # Resolve `application_name` to exactly one Application `id` (guards against a shared
151+ # database where several apps' ids share the same trailing path segment).
152+ self ._app_id : str = self ._resolve_application_id ()
153+
145154 # The application's module `_module` keys, used to scope every query to this app.
146155 self ._modules : List [str ] = self ._load_module_keys ()
147156
@@ -177,12 +186,39 @@ def _check_schema_version(self, expected: str | None = None, found: str | None =
177186 f"graph schema { found !r} in database, this SDK speaks { expected !r} — re-analyze with codeanalyzer-typescript>=1.0.0"
178187 )
179188
180- def _load_module_keys (self ) -> List [str ]:
189+ def _resolve_application_id (self ) -> str :
190+ """Resolve ``application_name`` to exactly one ``:Application`` node's ``can://`` id.
191+
192+ The suffix match (``a.id ENDS WITH "/" + $app``) can bind multiple applications in a
193+ shared database — e.g. two repos whose ids both end in ``/frontend`` — which would
194+ silently merge their module scopes. Raise :class:`CodeanalyzerUsageException` unless the
195+ match is unique; the caller disambiguates by passing a longer trailing path (any suffix
196+ of the ``can://`` id starting at a ``/`` boundary works as ``application_name``).
197+ """
181198 rows = self ._run (
182- 'MATCH (a:Application) WHERE a.id ENDS WITH "/" + $app '
183- "MATCH (a)-[:TS_HAS_MODULE]->(m:TSModule) RETURN m._module AS k" ,
199+ 'MATCH (a:Application) WHERE a.id ENDS WITH "/" + $app RETURN a.id AS id ORDER BY id' ,
184200 app = self .application_name ,
185201 )
202+ if not rows :
203+ raise CodeanalyzerUsageException (
204+ f"no :Application found whose id ends with '/{ self .application_name } ' — check application_name (the --app-name the graph was loaded with)."
205+ )
206+ if len (rows ) > 1 :
207+ candidates = ", " .join (r ["id" ] for r in rows )
208+ raise CodeanalyzerUsageException (
209+ f"application_name '{ self .application_name } ' is ambiguous: it matches { len (rows )} applications in this database ({ candidates } ). "
210+ "Pass a longer trailing path of the intended application's id to disambiguate."
211+ )
212+ return rows [0 ]["id" ]
213+
214+ def _load_module_keys (self ) -> List [str ]:
215+ # VERIFY(2.0.0-e2e): every project-owned node is assumed to carry a `_module` provenance
216+ # property (as in the pre-2.0.0 projection); all the `x._module IN $mods` scoping below
217+ # depends on it — validate against a live 1.0.0 graph (Task 9).
218+ rows = self ._run (
219+ "MATCH (a:Application {id: $app_id})-[:TS_HAS_MODULE]->(m:TSModule) RETURN m._module AS k" ,
220+ app_id = self ._app_id ,
221+ )
186222 return [r ["k" ] for r in rows ]
187223
188224 # -----[ child-fetch helpers (reconstruction) ]-----
@@ -195,7 +231,7 @@ def _callsites_of(self, signature: str) -> List[TSCallsite]:
195231
196232 def _callable_full (self , props : Dict [str , Any ]) -> TSCallable :
197233 sig = props ["signature" ]
198- # Symbol-keyed containers are keyed by signature (matching the analyzer's dict keys).
234+ # Nested-declaration containers are keyed by signature (matching the analyzer's dict keys).
199235 inner_callables = {p ["signature" ]: self ._callable_full (p ) for p in self ._children (sig , "TS_DECLARES" , "TSCallable" )}
200236 inner_classes = {p ["signature" ]: self ._class_full (p ) for p in self ._children (sig , "TS_DECLARES" , "TSClass" )}
201237 return R .callable_ (
@@ -309,7 +345,7 @@ def get_typescript_module(self, file_path: str) -> TSModule | None:
309345 if not rows :
310346 return None
311347 props = rows [0 ]["p" ]
312- # All symbol containers are keyed by signature (matching the analyzer's dict keys).
348+ # All declaration containers are keyed by signature (matching the analyzer's dict keys).
313349 classes = {p ["signature" ]: self ._class_full (p ) for p in self ._module_decls (file_path , "TSClass" )}
314350 interfaces = {p ["signature" ]: self ._interface_full (p ) for p in self ._module_decls (file_path , "TSInterface" )}
315351 enums = {p ["signature" ]: R .enum (p ) for p in self ._module_decls (file_path , "TSEnum" )}
@@ -488,6 +524,10 @@ def get_class_call_graph(self, qualified_class_name: str, method_signature: str
488524
489525 def get_class_hierarchy (self ) -> nx .DiGraph :
490526 """Inheritance/implementation graph: an edge child → base for every base_class."""
527+ # VERIFY(2.0.0-e2e): hierarchy is read from the `base_classes` / `implements_types` array
528+ # properties (as pre-2.0.0), not the TS_EXTENDS / TS_IMPLEMENTS edges — validate against a
529+ # live 1.0.0 graph (Task 9); this also covers get_extended_classes,
530+ # get_implemented_interfaces and get_all_sub_classes.
491531 graph = nx .DiGraph ()
492532 rows = self ._run (
493533 "MATCH (n:CanNode) WHERE (n:TSClass OR n:TSInterface) AND n._module IN $mods " "RETURN n.signature AS sig, n.base_classes AS bases" ,
@@ -513,6 +553,9 @@ def get_calling_lines(self, target_signature: str) -> List[int]:
513553 return [r ["line" ] for r in rows ]
514554
515555 def get_call_targets (self , source_signature : str ) -> Set [str ]:
556+ # VERIFY(2.0.0-e2e): falls back to `cs.method_name` when a call body node has no resolved
557+ # `callee`; whether TSBodyNode carries `method_name` is unconfirmed — validate against a
558+ # live 1.0.0 graph (Task 9).
516559 rows = self ._run (
517560 "MATCH (c:TSCallable {signature: $sig})-[:TS_HAS_BODY_NODE]->(cs:TSBodyNode {kind: 'call'}) " "RETURN cs.callee AS cosig, cs.method_name AS mn" ,
518561 sig = source_signature ,
0 commit comments