Files
agenticCode/x-docs/roadmap.md
Ingo Schnabel d4b14a3b6d Bug fixes
2026-07-27 16:20:42 +02:00

45 KiB
Raw Blame History

AgenticCode Roadmap — Open Tasks

This is a living task list of the remaining work. Completed features have been moved to x-docs/features.md (with full implementation notes); this file tracks only items that are still open.

The Web UI initiative (items 48–51) is largely delivered — backend prereqs 48–50 and frontend milestones M0–M5 are done (see x-docs/features.md); only M6 (scale & polish) remains, and it is postponed as of 2026-07-15. Bug #58 (stale nodes survive refresh), #59 (shared field tokenizer) and #43 (hash-based auto-invalidation + deleted-file sweep) were completed 2026-07-15; #61 (comments parsed as CALLNAT targets), #62 (data literals as false MODULE call targets), #63 (CALLNAT inside a string literal) and #64 (ingest summary contradicted the graph; dispatch guards lost their VALUE alternatives) 2026-07-16 (see x-docs/features.md). #24 (the stale-file sweep scanned the whole AstNode label once per file — a full upms refresh went from ~90 min, incomplete, to 179 s) and #25 (batch persist, already implemented) were completed 2026-07-16. A manual WGEAGB0S endpoint audit against the Natural source (2026-07-16) closed #65 (?depth= now counts module hops — it had silently reported "no DB access" for a module two calls from its tables) and #66 (the API dropped the copycode line/file provenance the graph carries, so a .cpy line was served as a host line). #69 (edges on the same line number from different files collapsed — data loss, found by #66's own fixture) and #70 (placeholder field resolution dropped the copycode provenance, so item 66 only worked for bare field references) were completed 2026-07-16, and #68 (field-flow bounded on raw CALLS hops — it reported "nothing consumes this field" for a consumer called from inside a subroutine, and introduced the derived CALLS_MODULE edge) and #67 (call-tree reported a direct dependency as 3 hops deep, and now flags truncated when its traversal budget cuts) 2026-07-17. The whole raw-hop depth family (65/67/68) and the copycode-provenance family (66/69/70) are closed. (Item 67's follow-up #71 was retracted 2026-07-17: it rested on a measurement error of mine — the correct count is 0, and the parser cannot produce the edge it assumed. See "Not a bug".) A VMULTMN4 audit (2026-07-17) found #72 (a nested DECIDE dropped the outer guard, so the dispatch table stated an incomplete condition as complete — 178 upms modules); #72 is fixed 2026-07-17 (see x-docs/features.md) and left #73 (a NONE branch is a negation no guard chain can express) and #74 (a resolved and a placeholder edge for one statement coexist — exposed, not caused, by #72). Item 26 (MCP session reliability) remains postponed as of 2026-07-13. All remaining open items are postponed; revisit when prioritised.

Web UI — code understanding & navigation

A React/TypeScript web UI (ac-ui/) for navigating the AgenticCode graph to migrate legacy Natural to Java. Full vision/architecture: x-docs/ui-proposal.md. Backend prereqs 48–50 and frontend milestones M0–M5 (+ the M6 explorer regex filter) are DONE — see x-docs/features.md. Only M6 scale/polish remains:

  • 51 (M6). Scale & polish (POSTPONED 2026-07-15) — virtualisation/large-graph performance, multi-project, auth, theming, export (SVG/PNG/report). (The rest of item 51 is complete; see x-docs/features.md for M0–M5 detail.) Key open risks carried from the earlier milestones: auto-ingest latency on large projects (deep-ingest concurrency cap defaults to 2); the STALE_SOURCE warm side-effect (a warming query re-ingests + re-hashes a module and thereby clears its staleness — treat ingest status as "true at query time"); node-id instability after re-ingest (the UI keys on name + sourceFile); auth/multi-user and the deploy model still open.

Lazy / deferred ingest (three-tier model)

Reworks ingest from eager whole-project parsing into a lazy, on-demand model. Three tiers: Tier 1 = cheap eager reference index (per file: nodes, identifiers, coarse call/DB references — no deep bodies); Tier 2 = lazy deep ingest (control flow, statement-level dataflow, precise reads/writes) triggered on demand; Tier 3 = source served from the filesystem, no longer stored on nodes. Reverse queries (callers, search_identifier, flow_backward) stay answerable because Tier 1 pre-indexes coarse references globally.

Items 36–43 are done (Tier-1 reference index + tri-state status, Tier-2 lazy deep-ingest, depth/node caps, unresolved-reference nodes, Tier-3 source-from-disk with stale check, the refresh surface, and hash-based auto-invalidation) — see x-docs/features.md. No open items remain in this track.

Ingest performance

(Items 24 — the stale-file sweep's missing (project, sourceFile) index, the actual persist bottleneck — and 25 — batch persist, found already implemented — completed 2026-07-16 and moved to x-docs/features.md. A full upms call-graph refresh (6311 files) now takes 179 s end-to-end (~63 s parse, 104.5 s persist across 32 batches, ~12 s finalize), against ~2.5–2.8 min per batch before. The parked "parallel parse phase" idea was implemented 2026-07-18 (item 24) — see x-docs/features.md. No open items remain in this track.)

Known bugs

  • 75. CONTAINS is not acyclic — 22 self-loops and 162 two-cycles in upms (found 2026-07-17 while root-causing item 74; cause NOT established — do not treat the notes below as settled). The containment hierarchy that dozens of queries traverse with CONTAINS* contains cycles:

    (IF  @ JX0031N0.nat:781-966) -[:CONTAINS]-> (FOR @ YFRAMBC0.cpy:65-15)
    (FOR @ YFRAMBC0.cpy:65-15)   -[:CONTAINS]-> (IF  @ JX0031N0.nat:781-966)
    

    Neo4j's variable-length patterns use trail semantics (no relationship repeats in a path), so queries terminate rather than hang — but the blow-up is real: three probes using an unbounded CONTAINS* over this region were killed at a 2-minute timeout during the item-74 investigation. Candidate, unconfirmed: copycode CONTROL_FLOW nodes are shared by every including module (one node per .cpy line), and a .cpy that opens a block it does not close (YFRAMBC0.cpy opens FOR at line 65; the END-FOR lives in the includer) gets containment edges from every includer's nesting context accumulated onto that one shared node. Related: 625 nodes have endLine < startLine (531 DB_ACCESS, 94 CONTROL_FLOW) — the FOR above is 65 -> 15. But this explains only 26 of 162 cycles and 4 of 22 self-loops, so it is not the main cause. Left open on purpose rather than guessed at. The cycles are not confined to the statement tree: the DATA_STRUCTURE node named "," in BSUPLFN0.nat (itself a parser artifact worth its own look) carries self-loops, so the INCLUDES -> field walk the bare-field resolvers do runs through cyclic ground too. That is why item 77's INCLUDE_FIELD_DEPTH bound is a correctness requirement, not a tuning knob — an unbounded CONTAINS* there is what killed the probes above. (Item 77's redirect does not traverse this region: src for a placeholder edge is only ever FUNCTION (157,616 edges) or MODULE (35,572) — never CONTROL_FLOW — and all 16,231 such functions are direct CONTAINS children of a module, so its *0..1 bound avoids the cycles entirely.)

    2026-07-17 — concrete impact established and fixed for the dynamic-CALLNAT family (pending corpus re-verify). The blow-up is not merely theoretical: a whole-root deep refresh of upms wedged finalize step 17 (resolve-dynamic-callnat-intra-indirect) for ~2 h without completing, which blocks every later step — including item 77's bare-field resolution, so item 77 could not be corpus-verified. Measured cause: that step joins three unbounded (caller:MODULE)-[:CONTAINS*0..]-> anchors, and the resulting per-caller path enumeration over the cyclic copycode region is cubic — a read-only probe of the exact query for a single caller (DAGNTFN0) did not finish in 60 s. Fix: the six dynamic-CALLNAT resolvers (RESOLVE_DYNAMIC_CALLNAT_INTRA / …_INDIRECT / …_CROSS

    • their …_SCOPED variants in CypherQueries) no longer descend CONTAINS to find a module's own statements. A MODULE is 1:1 with its sourceFile (verified: 3587 files, max one module each), and a module's CALLNAT sites and WRITES statements all carry that same sourceFile, so the anchors become sourceFile-equality hash-joins that cannot cycle. The dispatch variable in the cross-module resolver can live in an included PDA, so it is scoped to the caller's own file or a data structure the caller INCLUDES (matching by name alone would pull in 20958 unrelated same-named vars; the scope filter keeps the 307 in-scope ones). Proven equivalent on upms: resolve-dynamic-callnat-intra yields the identical 31 resolved (caller, target, lineNo) triples project-wide, and the rewritten indirect step completes project-wide in ~6 s. Existing dynamic-dispatch ITs (intra / indirect / cross / scoped / unresolved- survival) stay green. Still to do: deep-recreate upms and confirm finalize reaches 36/36; then finish item-77 corpus verification. This does not remove the underlying CONTAINS cycles — other CONTAINS* traversals remain exposed if a future step joins several of them; the cycles themselves (parser line-range/shared-copycode artifacts) are still open above.

    2026-07-18 — same blow-up confirmed on the READ path (frontend-facing, NOT yet fixed). Only the finalize write-queries were rewritten above; the runtime read-queries the UI (ac-ui) renders still use unbounded (m:MODULE)-[:CONTAINS*0..]->(src). Measured live against server v71 on a heavy module (ACCNPE01): callees >2 min / hangs, digest >10 s timeout, context >10 s timeout, callers ~3.6 s; light modules (BMTABBP0) and db-accesses/call-tree/graph/functions stay <1.2 s. So the UI's Callees / module-overview / context panels spin on large modules. Same cure as the dynamic- CALLNAT fix (src.sourceFile = m.sourceFile hash-join, INCLUDES-scoped for variables). Affected read constants: CALLEES, CALLERS, DIGEST/CONTEXT query, FUNCTION_CALLERS, DB_ACCESSES, variable READS/WRITES, *_FOR_MODULES. 2026-07-19 — fixed. Rather than the sourceFile hash-join, the read queries use a tighter, provably equivalent bound: every {@code CALLS}/{@code READS}/{@code WRITES}/{@code DB_ACCESS}-parent edge source is a {@code MODULE} (depth 0) or a {@code FUNCTION} that is a direct {@code CONTAINS} child of the module (depth 1) — verified corpus-wide (0 sources deeper, 0 non-direct-child edge-source functions, and {@code DB_ACCESS} parents are only {@code FUNCTION}/{@code MODULE}, never {@code CONTROL_FLOW}). So (m)-[:CONTAINS*0..]->(src) becomes (m)-[:CONTAINS*0..1]->(src), which returns the identical set but cannot walk the cyclic copycode region. 20 read-side traversals updated (callees, MODULE_HOP_OUT(+ wiring), DISPATCH_TABLE, EGO_NEIGHBORS_*, VARIABLE_ACCESSES, DB_ACCESSES(+FOR_MODULES), SQL_STATEMENTS(+FOR_MODULES), FUNCTION_CALLERS, SEARCH_BY_VALUE(+_CONTAINS), fieldFlow, BUILD_CALLS_MODULE, FLOW_FRONTIER_SOURCE_FILES). Finalize/resolve queries left as-is (they completed). Guarded by ReadPathBoundedTraversalIT (EXPLAIN plan asserts no unbounded CONTAINS expand in callees); full IT suite green (193/0/0). No recreate needed — a query-only change against the existing graph. Verified live (v76): ACCNPE01 callees >2 min → 0.16 s, digest >10 s → 3.3 s, context **>10 s → 3.1 s; WGEAGB0S calleesunchanged (7). The underlyingCONTAINS` cycles (parser artefacts) still exist, but both the finalize and the read consumers are now bounded — item 75 no longer has a practical impact.

  • 73. A NONE/ANY branch is reported under its enclosing guard alone — the condition is a negation no guard chain can express (found 2026-07-17 while fixing item 72; item 72 does not fix this). NaturalParser's VALUE_RESET clears a DECIDE's active value on NONE/ANY, so an assignment inside such a branch is attributed to the enclosing guard chain only. Under VALUE 'TABL' → DECIDE ON #FIELD-NAME → NONE → MOVE ..., item 72 now reports guards = [#SHORT-VIEW='TABL'], which reads as "happens for all of TABL". The truth is "#SHORT-VIEW = 'TABL' AND NOT (#FIELD-NAME = any of the branch's VALUEs)". This is the same complaint item 72 makes — a condition served as complete when it is not — so item 72's guards is not a total answer, only a strictly better one. A guard chain is a conjunction of equalities by construction; expressing this needs a negated link (e.g. a DispatchGuard with negated=true carrying the sibling branches' values), which is a model change, not a parser tweak. (Unmeasured: how many NONE branches actually contain assignments — many are IGNORE. Worth counting before investing.)

(Fixed bugs 55, 57, 58, the dossier field-ordering fix, and the raw-hop depth family — 65, 67 (call-tree), 68 (field-flow) — plus 69/70 (edge identity + copycode provenance) moved to x-docs/features.md. #71 is a known imprecision left behind by item 67 rather than a wrong answer; #72 is a real wrong answer, found by the 2026-07-17 VMULTMN4 audit.)

  • Not a bug — retracted 2026-07-17 (was #71): "external subroutine calls do not count as a module hop" rested on a bad measurement of mine, not on the code. I counted 4,808 "genuine external subroutine calls" in upms with (ma)-[:CONTAINS*0..]->(src)-[:CALLS]->(f), applying the single-owner filter to the target but not to the source: for a copycode-shared source function (L4N-ENTER is CONTAINSed by 137 modules) that enumerates all 137 as ma, each differing from the target's owner. The correct test — source and target single-owner — returns 0. And it must: resolvePlaceholderTargets filters ph.type IN ['MODULE', 'DATA_STRUCTURE'], so a FUNCTION placeholder is never resolved across modules; a PERFORM to another module's subroutine stays an unresolved placeholder (6 in upms) and never becomes a cross-module CALLS edge. A path therefore cannot enter a module at a FUNCTION node, which was #71's entire premise. (The real, tiny gap: those 6 unresolved placeholders. Natural external subroutines are a language feature this parser does not resolve — worth its own item if the corpus ever needs it.)

  • 98. View aliases declared in a LOCAL USING data area are still reported as tables (found 2026-07-27 while re-verifying item 95). Item 95's alias pre-scan is per-module, over the copycode-expanded lines — but a LOCAL USING data area is a separate module, so a view declared there is invisible to it. Live example: YGEAGBNH.nat:2617 does FIND (1) VDB2-VERSIS_GENAGREE, and the alias is declared in src/manual/local_data_area/new/YGEAGVL1.lda, not in YGEAGBNH — so db-accesses still reports VDB2-VERSIS_GENAGREE instead of VERSVW_GENAGREE. It is the last such row in the WGEAGB0S closure (58 → 1), but 348 view aliases are declared corpus-wide, so the class is probably wider than this one instance. Not a parser fix: parseDataArea already emits DATA_STRUCTURE --USES_TYPE--> DB_TABLE for a .lda/.pda view, so the graph holds the link. Likely shape: a post-ingest enricher redirecting a DB_TABLE whose name matches a view-alias DATA_STRUCTURE onto the table that structure USES_TYPE, then reaping the orphaned alias node. Sizing it properly wants a GET /db-tables listing endpoint, which does not exist yet.

  • 97. call-tree leaked the dynamic-call placeholder a manual override only hides (2026-07-27, third WGEAGB0S deep API audit). call-tree for WGEAGB0S listed #GETSHORT-MODUL — a variable (YGEAGGNH.nat:443, CALLNAT #GETSHORT-MODUL) — as a MODULE in the closure, while callees for the same module correctly reported only the resolved target YGEAGGN0. Cause: a manual override does not delete the marker edge to the variable-named placeholder, it sets manualHidden = true and relies on the read queries to suppress it (DELETE_DYNAMIC_CALLNAT_PLACEHOLDER_EDGES). callees/callers filter it; the BFS behind call-tree did not — MODULE_HOP_OUT/MODULE_HOP_OUT_WIRING did not even bind the relationship. Everything driven by that BFS inherited the pollution (graph, db-accesses?depth=N, sql-statements?depth=N). Done: both hop queries bind r and apply coalesce(r.manualHidden, false) = false, matching callees/callers. Characterization IT DynamicCallOverrideIT#callTreeHonoursTheOverrideLikeCallees (placeholder present → override → absent → reset → present again); verified red against the pre-fix query.

  • 96. Natural UPDATE(ref.) / DELETE(ref.) were dropped, hiding every access layer's write path (2026-07-27, third WGEAGB0S deep API audit). 34 statement sites across 13 of the 65 modules in the WGEAGB0S closure — every Y****MN0 CRUD module — produced no WRITES edge, so db-accesses showed them as read-only plus a single STORE. Cause: DB_WRITE's (?!\() guard (added by item 90 to stop a phantom (OLD.) table) suppressed the phantom but never recovered the real table, and DELETE was only handled in its SQL DELETE FROM form. Done: new DB_WRITE_BY_REF plus a pre-scan mapping each FIND/READ statement label to its (alias-resolved) table; an unresolvable reference still records nothing, so item 90's no-phantom guarantee holds — its two tests stay green unchanged and now serve as the negative cases. Shared with NaturalCoarseScanner so tier-1 and deep agree. Tests: NaturalParserTest#updateAndDeleteByReferenceResolveToTheEnclosingLoopTable, #byReferenceWriteWithoutAResolvableLoopNamesNoTable, NaturalCoarseScannerTest, IT NaturalViewAliasDbAccessIT. A label may also introduce a SQL SELECT loop rather than a FIND (YELEMMN0, YMULTMN0 hold their record that way) — those resolve through the FROM clause; #byReferenceWriteResolvesThroughALabelledSelectLoop. Verified on live upms: all 34 by-reference sites in the WGEAGB0S closure now recorded, 0 missing.

  • 95. Natural view aliases were reported as DB tables (2026-07-27, third WGEAGB0S deep API audit). db-accesses named the Natural view variable of a DML statement, not the DDM it is declared over: 58 rows across 13 of the 65 modules in the WGEAGB0S closure, 32 alias names standing in for 20 real tables. Worst effects — the generator's boilerplate alias NEXT-VIEW became one DB_TABLE node shared by 11 modules meaning 11 different tables (and reporting no columns), and 11 VDB2-*-VLOG aliases hid every write to VERSVW_LOGFILE, so "who writes the audit log?" answered nothing. Cause: VIEW OF was only recognised in parseDataArea (.pda files); parseModule — which parses every .nat — never built an alias map, and the DML branches passed the operand verbatim to dbTable(...). Done: VIEW_DECL pre-scan over the copycode-expanded lines feeds resolveViewAlias into the DB_WRITE/DB_READ branches; dbTable() now upper-cases (Natural is case-insensitive and DB_TABLE merges on the name). Shared with NaturalCoarseScanner so a shallow and a FULL module cannot report different names for the same statement. Tests: NaturalParserTest#viewAliasResolvesToTheUnderlyingTable, NaturalCoarseScannerTest, IT NaturalViewAliasDbAccessIT (incl. the same alias in two modules resolving to two tables). Verified on live upms: alias rows in the WGEAGB0S closure 58 → 1, the phantom NEXT-VIEW node gone, VERSVW_LOGFILE reachable for the first time. The remaining row is the cross-file case, item 98.

  • 94. call-tree no longer enumerates paths; followWiring usable again (2026-07-20, JX0034N0 ↔ MultiTableImportJob functional comparison). call-tree?followWiring=true timed out on pur at depth ≥ 2 (>120s; depth 1 already took 5.3s), which made the Java wiring closure unobtainable. Measured cause — a single quantified path pattern over CALLS|INJECTS|REFERENCES, bounded by maxDepth × (1 + internalBudget) (= 42 at depth 2), recovering each target's depth as min(#MODULE nodes on path) - 1, i.e. by enumerating every path. With the CHA-materialized wiring edges (items 31/92) that is combinatorial:

    rawBound Java, followWiring, depth 2 Natural JX0034N0, depth 5
    3 1.6s, 71 targets —
    4 1.7s, 71 targets 2.6s, truncated (42)
    6 18.9s, 71 targets 3.2s, truncated (79)
    8 / 12 >120s 2.2s / 2.4s, truncated (120/172)
    21 >120s 3.9s, converged (182)
    42 (production) >120s 3.2s, 182

    So the budget is necessary for Natural (whose result converges only near 21) and useless for Java (converged at 3) — lowering it globally would silently truncate Natural, the exact failure its javadoc warns about. The blow-up comes from the wiring edges, which JavaParser.addWiringEdges and the CHA steps only ever emit class-to-class, so they can never reach a FUNCTION. Fix: split the query. MODULE rows now come straight from the moduleDepths BFS (its hop index is the module-hop depth — verified equal to the old query's module set: 71/71 for Java, 46/46 for Natural), and only FUNCTION rows still traverse, CALLS-only and bounded within one module. Behaviour change: the budget can no longer hide a module whose call site sits behind a long internal PERFORM chain — DEPTHLEAF is now reported at depth 1, which also removes a standing contradiction with db-accesses/sql-statements, whose module set always came from the same BFS. truncated accordingly now means "some module's internal subroutine chain may be cut off". Covered by CallTreeTruncationIT (both tests).

    Measured live after deploy — call-tree?followWiring=true on MultiTableImportJob: depth 2 1.9s (was >120s) with the same 71 modules, depth 6 3.0s, depth 10 2.3s converging at 852 modules. On upms/JX0034N0 at depth 5 the module set grew 46 → 53 with nothing lost; the seven that had been hidden are NDBERR, NDBNOERR, USIX009N, USIX052N, USIX053N, YELEMGN0, YLITEMN0. They are real: USIX052N is CALLNATed by ISI173N0 (line 474), itself a direct callee of JX0034N0, so it sits at module depth 2; and YLITEMN0 was already reported by db-accesses?depth=5 as a via, which is the contradiction this item removes.

  • 93. Transitive db-accesses lost the DECLARES rows (2026-07-20, JX0034N0 ↔ MultiTableImportJob functional comparison). DB_ACCESSES resolves a table from three sources — READS/WRITES, an entity's own MAPS_TO, and a repository's repositoryEntity (item 32) — but its transitive counterpart DB_ACCESSES_FOR_MODULES (item 65) only ever had the first. The transitive view was therefore not a superset of the direct one: asking the same module with depth silently dropped its table. Minimal repro: db-accesses on MultiTableEntryEntity returns multi_table_entry/DECLARES, db-accesses?depth=1 on that same module returns []. Consequence: a Java caller's transitive db-accesses came back empty even though the entity it persists through maps to a real table, which made the Java side of a Natural↔Java DB comparison impossible to obtain from the API. Fix: DB_ACCESSES_FOR_MODULES now carries the same three UNION branches, with via naming the module that declares the table. SQL_STATEMENTS has no such branches, so SQL_STATEMENTS_FOR_MODULES needed no change (verified). Covered by JavaRepositoryOwnTableIT.entityTableAlsoResolvesInTheTransitiveView / repositoryTableAlsoResolvesInTheTransitiveView (both red before the fix).

  • 92. Java inheritance/CHA wiring: three defect classes fixed (2026-07-19, MultiTableImportJob deep API audit — manual Java source pass, project pur). Three systemic errors in the callees/wiring materialization, all found by comparing callees against source:

    • A — inherited INJECTS/REFERENCES lost their origin file (635× in the MTIJ closure, 255 with a lineNo past the caller file's end). LINK_REFERENCES_TO_SUBCLASSES/LINK_INJECTS_TO_SUBCLASSES (item 31) copied the base class's lineNo onto the subclass edge but never set originFile, so the callees sites (coalesce(r.originFile, source.sourceFile)) fell back to the subclass file. Worked example: MultiTableImportJob reports PurBatchJobListener REFERENCES lineNo=133, but that file has 93 lines — line 133 is in AbstractPurBatchJob.java. Same as the Natural copycode bug (items 66/91), for Java inheritance. Fix: materialized edges now carry originFile = coalesce(r.originFile, base.sourceFile) + inheritedFrom = base.name.
    • B — CHA fanned constructor calls out to subtypes (48× phantom CONSTRUCTOR callees). LINK_CALLS_TO_IMPLEMENTATIONS applied class-hierarchy analysis to callKind='CONSTRUCTOR' CALLS, so new ArrayList<>() produced a phantom → InputConstraintHolder [CONSTRUCTOR] (it extends ArrayList), and new BaseException() fanned out to every exception subtype. A constructor is statically bound. Fix: AND coalesce(r.callKind,'') <> 'CONSTRUCTOR'.
    • C — a qualified same-name supertype resolved to self (3× self-EXTENDS). DateUtils extends org.apache.commons.lang3.time.DateUtils (and NumberUtils/StringUtils) were resolved by simple name to the project's own same-named class → a DateUtils EXTENDS DateUtils self-loop that also poisoned the inheritance materialization. Fix: JavaParser.supertypeName keeps the FQN when a qualified supertype's simple name equals the declaring class's own name; the materializers additionally guard sub <> base. The parser fix stops new self-edges, but a non-wiping refresh leaves the old self-EXTENDS behind (both endpoints are the surviving class node, so node reconciliation never sweeps it — the edge gap item 86 closed for Natural), so a delete-self-inheritance-edges enrichment step reaps any self-EXTENDS/IMPLEMENTS edge project-wide before the inheritance graph is traversed.
    • Infra: a delete-synthetic-inheritance-edges enrichment step reaps all resolvedVia:'INHERITANCE' edges before the three materializers rebuild them, so a non-wiping refresh picks up the new properties/gates (otherwise MERGE ... ON CREATE never updates a pre-existing edge). Query-only fixes for A/B + reap; parser fix for C. IT JavaInheritanceWiringIT (3 tests). No response-shape change — originFile flows through the existing callees sites.callSiteFile.
  • 91. db-accesses/workfile-accesses/sql-statements carry copycode provenance (2026-07-19, third WGEAGB0S deep API audit — manual source pass). A DB or work-file access whose statement lives in an INCLUDEd copycode was reported with a copycode-local lineNo and no file context, so the number read as a line of the host module. Concretely: the DB2 sequence read SELECT … FROM SYSIBM-SYSDUMMY1 lives in USIX043C.cpy at lines 31/39/45/51/57; db-accesses for the 9 including modules (YAPRFMN0, YCUACMN0, YLITEMN0, YMODAMN0, YMTABMN0, YMULTMN0, YPRODMN0, YRAMOMN0, YUGRPMN0) reported those as bare lineNos that land on each host's own comment/DEFINE DATA lines. Root cause: the provenance was already on the READS/WRITES edge (item 66 stamps originFile/viaCopycode/includedAt on every edge in CopycodePreprocessor.remap, persisted via SET r += e.properties), but the DB_ACCESSES/ WORKFILE_ACCESSES/SQL_STATEMENTS queries never returned it — exactly the gap item 85 closed for functions and item 66 for callees/variables. Fix (pure query + DTO, no re-parse of data): db-accesses and workfile-accesses now return sites: [{lineNo, sourceFile, viaCopycode, includedAt}] (new AccessSite record) alongside the kept lineNos; sql-statements gains sourceFile + viaCopycode from the DB_ACCESS node's (remapped) file. includedAt is stored as a string, so the site queries wrap it in toInteger(...). Direct and transitive (?depth>0, *_FOR_MODULES) variants. REST auto-serializes the records; MCP returns the same DTOs; the CLI is a JSON passthrough — all in sync. IT WorkfileAndCopycodeFunctionIT#dbAccessSiteNamesTheCopycodeFileForCopycodeSourcedAccess (host FIND vs copycode FIND → sites[0].sourceFile/viaCopycode distinguish the two).

  • 91. search/identifier?priorityModule= pins the caller's module into the page; deterministic order (2026-07-26, UI click-to-identify test). Click-to-identify sent search/identifier?name=&limit=25, but the query had no ORDER BY and paginated in incidental index order, so for a name declared in >25 modules (e.g. #I-LINE-LEV, 192 declarations) the open module's own declaration was truncated away and the popover falsely reported "0 in this module". Fix: SEARCH_IDENTIFIER gains $priorityModule — it does not filter (unlike module=) but computes a pinRank (0 for that module's file, else 1) and ORDER BY pinRank, sourceFile, startLine, so the local match survives the limit while the global list is preserved; ordering is now deterministic (it was undefined before). Delivered across REST (priorityModule), MCP search_identifier, CLI --priority-module, and the UI hook. IT IdentifierPriorityModuleIT (four modules sharing one LOCAL field, PRIO_ZZZ_TARGET sorts last: excluded at limit=2 without the pin, first in the page with it, and the full set still returned at a large limit — i.e. no filtering).

  • 90. DELETE no longer mis-parsed as a table write (2026-07-19, second WGEAGB0S deep API audit, Finding 5). Natural DML DELETE [(label)] deletes the current record of the enclosing READ/FIND loop and names no view, and the EXAMINE … DELETE [FIRST] clause is not a DELETE statement at all — but DB_WRITE captured the token after DELETE as a table, producing phantom FROM (108×, from SQL DELETE FROM <table>), (OLD.)/(*)/label refs (19×), and FIRST (7×) — and, for SQL, lost the real table (it sat after FROM). Fix: DELETE removed from DB_WRITE (STORE/UPDATE keep their view operand); a new DB_DELETE_FROM captures the SQL DELETE FROM <table> real table (mode DELETE); DELETE (label)/DELETE FIRST name no table. The same label-reference shape also affects UPDATE (label) (UPDATE (OLD.)/(HOLD-PRIME.)) and a (*) read operand — a view never starts with (, so DB_WRITE/DB_READ now carry a (?!\\() guard that rejects a parenthesized label reference (no phantom (OLD.)/(*) table) while UPDATE <view> still records the real view. Both parsers. Tests in NaturalParserTest (DELETE FROM, DELETE (OLD.), EXAMINE … DELETE FIRST, UPDATE (OLD.)). Follow-up (2026-07-19, final verification): one last (*) phantom survived, from the SQL SELECT parser, not DB_READ. A SELECT column list can contain a hyphenated Natural field whose last segment is literally FROM (YCOMIROW.DAT-CALC-FROM (*)); FROM_VIEW = \bFROM\s+(\S+) treated the hyphen as a word boundary, captured the trailing (*) as a phantom table, and — since the FROM view binds on the first match only — swallowed the real FROM VERSVW_COMISION clause below. Fix: FROM_VIEW now uses a negative lookbehind (?<![-\\w.])FROM (FROM must be a standalone SQL keyword, not an identifier tail) plus the (?!\\() operand guard. Both parsers. Test NaturalParserTest#sqlSelectColumnEndingInFromDoesNotShadowTheRealFromClause.

  • 89. READ WORK <n> (FILE keyword omitted) recognized as work-file I/O (2026-07-19, second WGEAGB0S deep API audit, Finding 4). The item-84 guard only matched READ WORK FILE; the corpus also writes READ WORK 1 ONCE RECORD … (289× project-wide) without FILE, which still fell through to DB_READ and produced a phantom DB_TABLE 'WORK' (154 accesses). Fix: the WORK [FILE] n guard and the WORKFILE_ACCESS/WORKFILE_DEFINE patterns now treat FILE as optional (guarded on a following digit, so a view whose name merely starts with WORK is unaffected). Both parsers; NaturalParserTest (READ WORK 1 ONCE RECORD).

  • 88. Finalize sweep deletes edgeless DB_TABLE/WORKFILE placeholder nodes (2026-07-19, second WGEAGB0S deep API audit). Companion to item 86: reaping a stale access edge left the placeholder node (sourceFile="", never node-swept) behind with degree 0 — invisible to db-accesses (edge-driven) but still surfacing in search_identifier?type=DB_TABLE and the DB-table inventory (observed: orphaned NUMBER/WORK/FIRST after items 84/87). New finalize step delete-orphaned-placeholder-tables (DELETE_ORPHANED_PLACEHOLDER_TABLES) removes any DB_TABLE/ WORKFILE with no relationships; degree-0 only, so a table any file still accesses (or a Java @Entity's MAPS_TO target) is kept. Covered by StaleTableEdgeReapIT.orphanedPlaceholderTableNodeIsDeleted.

  • 87. FIND NUMBER <view> no longer mis-parsed as a phantom DB_TABLE 'NUMBER' (2026-07-19, second WGEAGB0S deep API audit). FIND NUMBER <view> is a count-only FIND (natural-grammar.md §7.1); NUMBER is a statement keyword, not the accessed view — but the DB_READ regex (in both NaturalParser and NaturalCoarseScanner) captured it as the table name, so db-accesses reported a bogus NUMBER table and lost the real view (e.g. CON-DB2-AUTHPROF-USED-IN-AUTHSPC, NEXT-VIEW). Seen on 11 modules of the WGEAGB0S call tree (YAPRFMN0, YCUACMN0, YENTIMN0, YGARAMN0, YLITEMN0, YMODAMN0, YMTABMN0, YPRODMN0, YRAMOMN0, YTABLMN0, YUGRPMN0; 13 accesses; 182 FIND NUMBER occurrences project-wide). Same class as item 84's READ WORK FILE. Fix: DB_READ now skips the FIND options ALL/FIRST/NUMBER/UNIQUE and the RECORDS/IN/FILE noise words before the view (and allows a variable record-limit (operand), not just a literal). Covered by NaturalParserTest (FIND NUMBER MY_VIEW / FIND NUMBER IN FILE OTHER_VIEW). Item 86 reaps the existing NUMBER edges on the next refresh.

  • 86. Re-ingest reaps stale Natural DB_TABLE/WORKFILE access edges (self-healing) (2026-07-19, follow-up to items 84/85). A statement whose access target changed between parses orphaned its old READS/WRITES edge forever: the target is a placeholder (sourceFile="", never node-swept) and the source node survives, so neither the item-58 node sweep nor the target-keyed edge MERGE reaped it. Seen as the WORK db-access that lingered on USIX052N after the item-84 parser fix (a pre-fix READ WORK FILE→DB_TABLE 'WORK' edge), and it applies to any edited view name too. Fix: before re-merging a re-parsed Natural file's edges, DELETE_STALE_NATURAL_TABLE_ACCESS_EDGES drops its READS/WRITES edges to DB_TABLE/WORKFILE placeholders; the fresh parse (which always re-emits them) re-creates the current ones, unchanged ones round-trip identically. Scoped to language:'natural' source nodes — Java DB edges are resolver-built (RESOLVE_JAVA_DB_ACCESS) and untouched. Covered by StaleTableEdgeReapIT (edit READ VERSVW_OLD → READ VERSVW_NEW + READ WORK FILE, assert old view + WORK gone). (A clean re-ingest of upms already cleared the existing stale WORK; item 86 prevents recurrence on incremental refreshes.)

  • 84. Natural work-file access tracking (workfile-accesses) + READ WORK FILE no longer a phantom DB table (2026-07-19, WGEAGB0S deep API audit, Finding 2). READ WORK FILE n <buf> (sequential flat-file I/O) was matched by the (READ|FIND) <view> DB pattern in both NaturalParser and NaturalCoarseScanner, creating a bogus DB_TABLE 'WORK' READS access (seen on USIX052N in the WGEAGB0S call tree). Both DB_READ patterns now negative-lookahead WORK FILE, and READ/WRITE WORK FILE are modelled as first-class WORKFILE + WORKFILE_ACCESS nodes (analogue of DB_TABLE/DB_ACCESS), keyed by work-file number, with the record buffer on the READS/WRITES edge and the DEFINE WORK FILE n '<name>' physical name on the node. New GET /modules/{name}/workfile-accesses → [{workFile, physicalName, mode, recordBuffers, lineNos}], MCP workfile_accesses, CLI ac workfile-accesses. Covered by NaturalParserTest (READ + WRITE) and full-stack WorkfileAndCopycodeFunctionIT; mcp-api-usage/system-prompt docs updated.

  • 85. /functions items carry sourceFile + viaCopycode (copycode-provided subroutine provenance) (2026-07-19, WGEAGB0S deep API audit, Finding 1). A subroutine pulled into a module via INCLUDE was listed with declaredIn=the including module and the copycode's startLine/endLine but no file, so the lines pointed outside the module's own (shorter) file — e.g. ISIN0019 (59-line file) reported GET-FORMAT at 90–120, which actually live in ISIC0010.cpy. The FUNCTION node already stored the right sourceFile; the MODULE_FUNCTIONS/MODULE_FUNCTIONS_OWN/_INHERITED projections just dropped it. Now InheritedFunction/FunctionInfo expose sourceFile (+ derived viaCopycode = f.sourceFile <> m.sourceFile). Covered by WorkfileAndCopycodeFunctionIT; docs updated.

  • Module callers default is external-only; no MODULE self-loop (2026-07-19, WGEAGB0S deep API audit). Two coupled defects in CypherQueries.callers(scope): (1) the top-level main body's PERFORMs originate at the MODULE node, so scope=internal/default reported the module as its own caller (WGEAGB0S → WGEAGB0S), a self-loop callees never mirrors — fixed with AND caller <> m on the internal scope; (2) the default (scope=null) merged external callers with intra-module PERFORM wiring, so a module's own subroutines showed up as its "callers" — the default now maps to external (genuine incoming CALLNAT/inheritance only). context/digest (both call callers(…, null)) inherit the clean view; scope=internal still exposes function→function PERFORM wiring; callees unchanged. Covered by ModuleCallersSelfLoopIT (fails 2/3 before the fix). MCP callers tool description + REST endpoint doc + mcp-api-usage-ac-implementation.md updated. (Supersedes the earlier "Not a bug (verified): context.callers includes internal PERFORM callers — noisy but accurate" note.) Ego-graph direction=in for WGEAGB0S now returns its dynamic callers W-LST-N0/W-MNT-N0 (item 75). Payload direction is always REQUEST for PDA-derived contracts (a single interface PDA doesn't encode direction) — a documented limitation, not a bug.

    • Follow-up (2026-07-19, WGEAGB0S call-tree closure re-audit): the "external-only" default was only half-fixed. The default view still returned the calling FUNCTION node (a subroutine/method) whenever the call originated inside a subroutine rather than the main body — ModuleCallersSelfLoopIT missed it because its fixture caller CALLNATs from the main body, where the edge already starts at the MODULE. Measured on upms: 46/56 modules in the WGEAGB0S closure had FUNCTION-typed rows in the default callers, 29/56 had duplicate rows, and hot utilities were unusable (CDRANGE default callers = 500 rows / 3 distinct FUNCTION names / 0 module callers). Fixed by making the external branch of CypherQueries.callers(scope) roll every caller up to its owning MODULE via (callerModule:MODULE)-[:CONTAINS*0..1]->(source)-[r]->(m) and collect(DISTINCT …) — symmetric with how callees anchors its source side. Covered by new CallersRollupIT (caller invokes from inside a subroutine, twice → one rolled-up MODULE row with two aggregated sites, no FUNCTION leak); the old ModuleCallersSelfLoopIT, JavaWiringIT, JavaModulesExtendsFilterIT stay green.

Natural parser robustness

(Item 59 — shared field-declaration tokenizer — completed 2026-07-15; items 61 — comments parsed as CALLNAT targets — 62 — data literals as false MODULE call targets — and 63 — CALLNAT matched inside a string literal — completed 2026-07-16. All moved to x-docs/features.md. No open items remain in this track: the unanchored-CALLNAT family (#61 comments / #62 data literals / #63 string literals) is closed, and the shared lexical helpers now live in NaturalLines + NaturalFieldTokenizer so a fix lands in both ingest tiers at once.)

Agent API / MCP tooling gaps

(Done items 52, 53, 54, 56 moved to x-docs/features.md.)

  • 82. Manual override for unresolvable dynamic CALLNAT targets (human/agent-settable) (proposed + implemented 2026-07-19, from the WGEAGB0S deep-API audit; REST + MCP + ac CLI + Testcontainers ITs green). The dynamic-CALLNAT resolvers cannot follow every name-assembly pattern — e.g. YGEAGGNH.nat:443 CALLNAT #GETSHORT-MODUL where the name is built via MOVE 'YGEAGKEY' TO #GETSHORT-MODUL + MOVE 'GN0' TO SUBSTR(#GETSHORT-MODUL,6,3) → YGEAGGN0 (a real, ingested module). Such a call site leaves an unresolved placeholder (type=MODULE, sourceFile="", name = the variable). Add a REST + MCP + ac-CLI capability to list unresolved dynamic call sites and manually resolve a call site to one or more target modules (multiple targets = deliberate branches, each materialised as a real CALLS callKind=CALLNAT_DYNAMIC edge, provenance manual). Decisions (2026-07-19): callsite key = originFile + lineNo; overrides are persistent (own node type the refresh never deletes) and auto re-applied by an enrichment step after every refresh/deep-refresh; a reset endpoint clears manual overrides (one call site, or all). Related: this is the actionable counterpart to the Bug B consistency gap — callees/digest should also surface the unresolved flag that graph already exposes.

  • 83. Auto-resolver for string-assembled dynamic CALLNAT targets (SUBSTR/MOVE constant-folding) (proposed 2026-07-19, from the WGEAGB0S deep-API audit follow-up; implemented 2026-07-27). Many unresolved dynamic call sites are in fact statically foldable: the target name is built from literals only, e.g. the Y…GNH "GetShort" family (~44 modules) — MOVE 'YxxxxKEY' TO #GETSHORT-MODUL + MOVE 'GN0' TO SUBSTR(#GETSHORT-MODUL,6,3) → YxxxxGN0 — plus similar families (PXFRAA01.ST-PGM across the P…MP0 programs, the YFRAMBCK.cpy:27 group). Add an enricher that constant-folds a chain of MOVE <literal> and MOVE <literal> TO SUBSTR(var,pos,len) assignments feeding a CALLNAT var into the effective target, and resolves the edge automatically when that target is a real ingested module — so item 82's manual override is only needed for genuinely runtime-dependent names, not the deterministic string idioms. Must respect precedence: a manual override (item 82) still wins over an auto-fold. Deliver with characterization ITs (a minimal fixture per idiom) + the usual REST/MCP/CLI-visible effect (fewer unresolved sites; resolved callees/callers). Done: NaturalParser records MOVE '<lit>' TO SUBSTR(var,pos,len) as a WRITES on the base var carrying substrPos/substrLen; new RESOLVE_DYNAMIC_CALLNAT_FOLD (+_SCOPED) folds base literal + ordered overlays (reduce/left/substring) → resolved CALLS edge tagged folded=true; the direct-literal/indirect/cross resolvers now skip partial-slice writes (substrPos IS NULL). Precedence honoured by guarding the fold against :DynamicCallOverride sites plus a delete-folded-overridden-dynamic-callnat step before apply-manual. Characterization ITs in DynamicCallnatFoldIT (fold resolves YABALKEY+GN0@6/3 → YABALGN0; manual override wins); full dynamic-callnat regression 89/89 green. Docs in mcp-api-usage-ac-implementation.md.

  • 26. MCP session reliability (POSTPONED 2026-07-13) (investigated 2026-07-07, not fixed — see below) — mcp__agenticcode__module_functions (and potentially other MCP tools) intermittently failed with "the first message from the client must be initialize: tools/call" after a sequence of prior successful MCP calls in the same session, forcing a fallback to the equivalent REST endpoint. Findings: no session/idle-timeout is configured server-side (quarkus.http.idle-timeout unset), and application.properties only sets quarkus.mcp.server.server-info.*. The error shape (client sent tools/call without a fresh initialize) points at the MCP client's reconnect handling on a new SSE stream, not a server-side bug this codebase's config can fix. Left open pending either a quarkus-mcp-server-sse version bump with related fixes, or evidence this is in fact server-triggered.