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
neware class→class (MODULE→MODULE). EXTENDS/IMPLEMENTS/MAPS_TOare Java-only.INJECTS(CDI injection) andREFERENCES(X.classargument) are Java-only wiring edges (item J2), surfaced incallers/calleesasedgeKind.IMPLEMENTED_BY(item J3) is the derived inverse ofIMPLEMENTS(interface → concrete impl), built in enrichment; it drives the?resolveInterfaces=truehop oncallees/call-tree. Java MODULE nodes carry anisInterfaceproperty.OVERRIDDEN_BY(item J4) links a base-class methodFUNCTIONto the same-named overridingFUNCTIONin a subclass (virtual dispatch), built in enrichment; exposed viaGET /modules/{name}/functions/{fn}/overrides.- DB access (item J1): a JPA/Panache repository/
EntityManager/active-record call becomes aDB_ACCESSnode whose entity resolves to theDB_TABLE, materializingFUNCTION -READS/WRITES-> DB_TABLE(fordb-accesses) andDB_ACCESS -USES_TYPE-> DB_TABLE(forsql-statements) — the same shape Natural uses. - Java emits no
DATA_STRUCTUREedges.
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=2after a sidecar pass. FUNCTION.kindandexported;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
CALLSedges, shaped exactly like the Java parser's so every call-graph query and the placeholder rewiring apply unchanged. Aslice/constdeclaration is not a node in 192 (item 194). - Item 193: a generated web-service call is a
FUNCTIONwithkind=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 areFIELDs under itsDATA_STRUCTURE(dataType= the TS type,optional). A member call on an Endpoint instance carriescalleeMethod = <Class>.<member>. COUNTERPART_OF(item 193, cross-project, built by enrichment, never by a parser): endpointFUNCTION→ handlerFUNCTIONin a counterpart project (via=rest), generatedDATA_STRUCTURE→ JavaMODULEof the same simple name (via=dto),FIELD→FIELDby name (via=field). Rebuilt on every refresh of either project; the project node carries thecounterpartslist.- 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'sFIELD(Broker→ebene), edge propspath(broker.ebene),rootDto,kind(field/prefix),partial,component,attribute,via=binding,lineNo. Cross-file targets areFIELDplaceholders<Dto>.<field>withbinding=true,owner,field,targetModule, resolved exactly (module → structure → field) byresolve-binding-placeholderand 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 CSSMODULE -CONTAINS-> STYLEper rule withstyleKind=css,selector);STYLE -REFERENCES-> FIELD theme.<token>(via=theme,property) andFUNCTION -REFERENCES-> FIELD theme.<token>(via=theme,context). Cross-file token targets are placeholders resolved by exact name byresolve-theme-placeholder; an undeclared token keeps its placeholder (declared=falseinGET /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
PERFORMis function→function;CALLNATis module→module.- DB access is modeled two ways: direct
FUNCTION -READS/WRITES-> DB_TABLE(e.g.READ/STORE) and aDB_ACCESSstatement node for SQL/ADABAS statements (FUNCTION -CONTAINS-> DB_ACCESS -USES_TYPE-> DB_TABLE/ viewDATA_STRUCTURE). CONTROL_FLOWblocks are contained by their enclosing function (or module).INCLUDES/CONTROL_FLOWare Natural-only.DB_ACCESSis now emitted by both parsers (Natural SQL/ADABAS statements; Java JPA/Panache calls — item J1).