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

183 lines
9.5 KiB
Markdown

# 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`
```mermaid
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)
```mermaid
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 `FIELD`s 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):**
```mermaid
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`
```mermaid
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).