Files
agenticCode/x-docs/ast-graph-schema.md
Ingo Schnabel 10971915e9 Typescript
2026-09-23 08:15:53 +02:00

9.5 KiB

AST Graph Schema — Nodes & Edges per Parser

Visualizes the node types and edges each parser actually emits, verified against the edge(...) call sites in JavaParser and NaturalParser (not the documented schema — only what is produced today).

CALLS edges carry a language-aware callKind property (see the CallKind enum in ac-parser-core), surfaced as edgeKind by the callers/callees queries.

Neo4j labels (item 111a, 2026-08-05)

Every node carries two labels: AstNode, plus its NodeType name — :MODULE, :FUNCTION, :VARIABLE, :DATA_STRUCTURE, :DB_TABLE, :CONSTANT, :FIELD, :DB_ACCESS, :WORKFILE, :WORKFILE_ACCESS, :CONTROL_FLOW, :PAYLOAD_FIELD. Written by both persist paths and backfilled onto pre-existing nodes at startup.

The type is also still a type property, and every query currently reads that property. The label is the migration target, not yet the source of truth: filtering on type out of the property store cost ~935k of 1.6M dbHits (58%) in a profiled upms traversal, because the property sits in an 11-property chain, whereas a label lives in the node record. Query sites move to :LABEL one at a time under measurement (roadmap 111b); the property is dropped only once none read it (111c).

Generation stamps. Nodes and, since item 198, every parser-emitted edge carry ingestGen, the persist run that last wrote them (a re-emitted edge is re-stamped through its MERGE key). A deep refresh deletes a re-parsed file's edges whose stamp is older than the run's; edges the finalize builds (resolved counterparts, links) carry no stamp unless copied from a parser edge.

Writing your own Cypher: prefer MATCH (n:MODULE {project: $p}) over MATCH (n:AstNode {type: 'MODULE', project: $p}) — both are correct today, the first is cheaper.

Java — JavaParser

graph LR
    M[MODULE<br/>class / interface]
    F[FUNCTION<br/>method / constructor]
    P[VARIABLE<br/>parameter]
    FD[FIELD<br/>instance attribute]
    C[CONSTANT<br/>static final]
    T[DB_TABLE]
    A[DB_ACCESS<br/>JPA/Panache call]
    M -->|CONTAINS| F
    M -->|CONTAINS| FD
    M -->|CONTAINS| C
    F -->|CONTAINS| P
    F -->|" CALLS — callKind=METHOD_CALL<br/>(intra-class foo) "| F
    M -->|" CALLS — callKind=METHOD_CALL<br/>(cross-class other.foo) "| M
    M -->|" CALLS — callKind=CONSTRUCTOR<br/>(new Foo) "| M
    F -->|READS / WRITES| FD
    M -->|EXTENDS| M
    M -->|IMPLEMENTS| M
    M -->|" IMPLEMENTED_BY (interface→impl) "| M
    M -->|" INJECTS (@Inject / ctor) "| M
    M -->|" REFERENCES (X.class arg) "| M
    M -->|" MAPS_TO (JPA @Entity) "| T
    F -->|" READS / WRITES (J1) "| T
    F -->|CONTAINS| A
    A -->|USES_TYPE| T
  • Same-class calls stay function→function; cross-class calls and new are class→class (MODULE→MODULE).
  • EXTENDS / IMPLEMENTS / MAPS_TO are Java-only. INJECTS (CDI injection) and REFERENCES (X.class argument) are Java-only wiring edges (item J2), surfaced in callers/callees as edgeKind.
  • IMPLEMENTED_BY (item J3) is the derived inverse of IMPLEMENTS (interface → concrete impl), built in enrichment; it drives the ?resolveInterfaces=true hop on callees/call-tree. Java MODULE nodes carry an isInterface property.
  • OVERRIDDEN_BY (item J4) links a base-class method FUNCTION to the same-named overriding FUNCTION in a subclass (virtual dispatch), built in enrichment; exposed via GET /modules/{name}/functions/{fn}/overrides.
  • DB access (item J1): a JPA/Panache repository/EntityManager/active-record call becomes a DB_ACCESS node whose entity resolves to the DB_TABLE, materializing FUNCTION -READS/WRITES-> DB_TABLE (for db-accesses) and DB_ACCESS -USES_TYPE-> DB_TABLE (for sql-statements) — the same shape Natural uses.
  • Java emits no DATA_STRUCTURE edges.

TypeScript / React — TypeScriptParser (item 192)

graph LR
  M[MODULE<br/>.ts / .tsx file — name = root-relative path without extension]
C[MODULE<br/>.css file — name keeps .css]
F[FUNCTION<br/>function / component / hook / thunk / styled / class]
D[DATA_STRUCTURE<br/>interface / type / enum]
M -->|CONTAINS|F
M -->|CONTAINS|D
M -->|" REFERENCES (import; value = clause, specifier) "|M
F -->|" CALLS — same file, callSyntax=call|new|tagged|jsx "|F
M -->|" CALLS — cross-module, callKind=METHOD_CALL|CONSTRUCTOR,<br/>callerFn, calleeMethod, receiver, member "|M
  • Module properties: simpleName, workspace, moduleKind (ts/tsx/css), generated, generator, externalImports (npm packages, comma list — never placeholders), sourceHash, loc/sloc, ingestTier=2 after a sidecar pass.
  • FUNCTION.kind and exported; DATA_STRUCTURE.dataType = interface/type/enum.
  • Tier-1 (regex, Java) emits the shells and import placeholders; Tier-2 (Node sidecar on the TypeScript compiler API) replaces declarations/imports from the checker and adds the CALLS edges, shaped exactly like the Java parser's so every call-graph query and the placeholder rewiring apply unchanged. A slice/const declaration is not a node in 192 (item 194).
  • Item 193: a generated web-service call is a FUNCTION with kind=endpoint, outbound=true, httpMethod, restPath (base-less, like a Java handler's composed @Path), restBase, restUrl, backend, queryParams, requestType, responseType, generator, owner, member; an interface's properties are FIELDs under its DATA_STRUCTURE (dataType = the TS type, optional). A member call on an Endpoint instance carries calleeMethod = <Class>.<member>.
  • COUNTERPART_OF (item 193, cross-project, built by enrichment, never by a parser): endpoint FUNCTION → handler FUNCTION in a counterpart project (via=rest), generated DATA_STRUCTURE → Java MODULE of the same simple name (via=dto), FIELD → FIELD by name (via=field). Rebuilt on every refresh of either project; the project node carries the counterparts list.
  • Item 194 (Redux store):
graph LR
  M[MODULE<br/>slice file]
  S[STORE_SLICE<br/>name = reducer key, sliceName, stateType, store=true]
  SF[FIELD<br/>name = key.field, dataType, optional, store=true, slice, field]
  R[FUNCTION<br/>kind=reducer, reducerKind=reducer|case|matcher|default,<br/>name = action type, slice, trigger, actionType]
T[FUNCTION<br/>kind=thunk]
C[FUNCTION<br/>component / hook — any file]
M -->|CONTAINS|S
M -->|CONTAINS| R
S -->|CONTAINS|SF
R -->|" READS / WRITES — path, lineNo, via=reducer "|SF
R -->|" READS / WRITES — whole state "|S
T -->|" CALLS — callSyntax=extraReducer, actionType "|R
C -->|" READS — path, lineNo, via=useAppSelector|wrapper hook|getState "|SF

A consumer's read is emitted against a placeholder <key>.<field> (sourceFile="", store=true) and redirected by the finalize step resolve-store-placeholder onto the real field of the same type and name (unique by construction), which then drops the placeholder. A dispatched action creator is a cross-module CALLS edge with actionType and calleeMethod = the reducer's name.

  • Item 195 (DTO field bindings): FUNCTION(component/hook) -READS-> / -WRITES-> the generated interface's FIELD (Broker → ebene), edge props path (broker.ebene), rootDto, kind (field/prefix), partial, component, attribute, via=binding, lineNo. Cross-file targets are FIELD placeholders <Dto>.<field> with binding=true, owner, field, targetModule, resolved exactly (module → structure → field) by resolve-binding-placeholder and then dropped.
  • Item 196 (styling): the theme file's DATA_STRUCTURE theme (kind=theme) -CONTAINS-> FIELD theme.<token> (theme=true, token, tokenKind=path|constant, value, constant); FUNCTION(component) -CONTAINS-> STYLE (styleKind=sx|style|styled, element, properties, literals, dynamic, spread; a CSS MODULE -CONTAINS-> STYLE per rule with styleKind=css, selector); STYLE -REFERENCES-> FIELD theme.<token> (via=theme, property) and FUNCTION -REFERENCES-> FIELD theme.<token> (via=theme, context). Cross-file token targets are placeholders resolved by exact name by resolve-theme-placeholder; an undeclared token keeps its placeholder (declared=false in GET /theme).
  • No DB_* for TypeScript.

Natural — NaturalParser

graph LR
    M[MODULE<br/>source file]
    F[FUNCTION<br/>subroutine]
    V[VARIABLE]
    DS[DATA_STRUCTURE<br/>DEFINE DATA group]
    CF[CONTROL_FLOW<br/>IF / FOR / REPEAT / DECIDE]
    A[DB_ACCESS<br/>READ/FIND/STORE/SELECT...]
    T[DB_TABLE<br/>ADABAS view / SQL table]
    M -->|CONTAINS| F
    M -->|CONTAINS| DS
    DS -->|CONTAINS| V
    M -->|" INCLUDES (copycode / PDA) "| DS
    F -->|CONTAINS| CF
    F -->|" CALLS — callKind=PERFORM "| F
    M -->|" CALLS — callKind=CALLNAT "| M
    F -->|READS / WRITES| V
    F -->|READS / WRITES| T
    F -->|CONTAINS| A
    A -->|USES_TYPE| T
    A -->|USES_TYPE| DS
    V -->|USES_TYPE| T
  • PERFORM is function→function; CALLNAT is module→module.
  • DB access is modeled two ways: direct FUNCTION -READS/WRITES-> DB_TABLE (e.g. READ/STORE) and a DB_ACCESS statement node for SQL/ADABAS statements (FUNCTION -CONTAINS-> DB_ACCESS -USES_TYPE-> DB_TABLE / view DATA_STRUCTURE).
  • CONTROL_FLOW blocks are contained by their enclosing function (or module).
  • INCLUDES / CONTROL_FLOW are Natural-only. DB_ACCESS is now emitted by both parsers (Natural SQL/ADABAS statements; Java JPA/Panache calls — item J1).