76 KiB
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 surviverefresh), #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 theirVALUEalternatives) 2026-07-16 (seex-docs/features.md). #24 (the stale-file sweep scanned the wholeAstNodelabel once per file — a fullupmsrefresh went from ~90 min, incomplete, to 179 s) and #25 (batch persist, already implemented) were completed 2026-07-16. A manualWGEAGB0Sendpoint 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.cpyline 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-flowbounded on rawCALLShops — it reported "nothing consumes this field" for a consumer called from inside a subroutine, and introduced the derivedCALLS_MODULEedge) and #67 (call-treereported a direct dependency as 3 hops deep, and now flagstruncatedwhen its traversal budget cuts) 2026-07-17. The whole raw-hopdepthfamily (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".) AVMULTMN4audit (2026-07-17) found #72 (a nestedDECIDEdropped the outer guard, so the dispatch table stated an incomplete condition as complete — 178 upms modules); #72 is fixed 2026-07-17 (seex-docs/features.md) and left #73 (aNONEbranch 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.mdfor M0–M5 detail.) Key open risks carried from the earlier milestones: auto-ingest latency on large projects (deep-ingest concurrency cap defaults to 2); theSTALE_SOURCEwarm 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-idinstability after re-ingest (the UI keys onname + 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
-
107. Every module endpoint answers
200with an empty shell for a module that does not exist — indistinguishable from a real but empty module (found 2026-08-02,upmswebservice-layer audit; contradicts the documented404 MODULE_NOT_FOUND)Symptom. Measured against the live server,
upms:GET /modules/WXSPOD0S/digest → 200 {"name":"WXSPOD0S","description":null,"functionCount":0, "callers":{},"callees":{},"dbTables":[],"dataStructures":[]} GET /modules/NOSUCHMOD123/digest → 200 {"name":"NOSUCHMOD123", … identical shell … }Byte-identical answers apart from the echoed name — for a module that exists nowhere in the graph and for a name typed at random.
callees,call-treeandcontextbehave the same (all200).search/identifier?name=WXSPOD0S&type=MODULEcorrectly returns[], so the graph knows; only the module endpoints invent the row.agent-api-system-prompt.mdpromises404 NODE_NOT_FOUND / MODULE_NOT_FOUND — unknown id / module name.Why it matters — this produced a wrong analytical result, not just an ugly response. The question was "can any
W*webservice module reach the commission calculation?". Five dispatchers (WPOLIX0S,WGARCX0S,WACOMX0S,WCLAIX0S,WOBJPX0S) dispatch dynamically to 13 targets (WXSPOD0S,WXSDAD0S,WXSCMD0S,WCARLD0S,WXSCLD0S,WXSCLD2S,WXSFUD0S,WXSGAD0S,WACOMD0S,WCLAID0S,WOBJPD0S,WCATEX0S,WCATED0R).call-tree?depth=4on each returned200with 0 modules and 0 provenance hits, which reads as "analysed, nothing found". The truth is "not analysable" — all 13 source files are absent from the checkout (findfinds none). An agent that trusts the200concludes "these paths trigger no commission processing"; the honest answer is "unknown". Same failure mode as item 103: an incomplete answer that looks complete.Fix. Return
404 MODULE_NOT_FOUNDwhen noMODULEnode exists for(project, name)— the checksearch/identifieralready performs. If a node exists but its source is not ingested (placeholder,sourceFile = ""), that is a different state and deserves an explicit marker in the payload (placeholder: true/sourceFile: null) rather than an all-zeros body. Worth checking which other/modules/{name}/…endpoints share the shell (functions,db-accesses,data-structures,dispatch-tableall plausibly do —dispatch-tablereturning[]for a non-existent module is currently indistinguishable from item 108's real gap). -
75.
CONTAINSis not acyclic — 22 self-loops and 162 two-cycles inupms(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 withCONTAINS*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: copycodeCONTROL_FLOWnodes are shared by every including module (one node per.cpyline), and a.cpythat opens a block it does not close (YFRAMBC0.cpyopensFORat line 65; theEND-FORlives in the includer) gets containment edges from every includer's nesting context accumulated onto that one shared node. Related: 625 nodes haveendLine < startLine(531DB_ACCESS, 94CONTROL_FLOW) — theFORabove is65 -> 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: theDATA_STRUCTUREnode named","inBSUPLFN0.nat(itself a parser artifact worth its own look) carries self-loops, so theINCLUDES -> fieldwalk the bare-field resolvers do runs through cyclic ground too. That is why item 77'sINCLUDE_FIELD_DEPTHbound is a correctness requirement, not a tuning knob — an unboundedCONTAINS*there is what killed the probes above. (Item 77's redirect does not traverse this region:srcfor a placeholder edge is only everFUNCTION(157,616 edges) orMODULE(35,572) — neverCONTROL_FLOW— and all 16,231 such functions are directCONTAINSchildren of a module, so its*0..1bound avoids the cycles entirely.)2026-07-17 — concrete impact established and fixed for the dynamic-
CALLNATfamily (pending corpus re-verify). The blow-up is not merely theoretical: a whole-rootdeeprefresh ofupmswedged 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-CALLNATresolvers (RESOLVE_DYNAMIC_CALLNAT_INTRA/…_INDIRECT/…_CROSS- their
…_SCOPEDvariants inCypherQueries) no longer descendCONTAINSto find a module's own statements. AMODULEis 1:1 with itssourceFile(verified: 3587 files, max one module each), and a module'sCALLNATsites andWRITESstatements all carry that samesourceFile, so the anchors becomesourceFile-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 callerINCLUDES(matching by name alone would pull in 20958 unrelated same-named vars; the scope filter keeps the 307 in-scope ones). Proven equivalent onupms:resolve-dynamic-callnat-intrayields 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-recreateupmsand confirm finalize reaches 36/36; then finish item-77 corpus verification. This does not remove the underlyingCONTAINScycles — otherCONTAINS*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) anddb-accesses/call-tree/graph/functionsstay <1.2 s. So the UI's Callees / module-overview / context panels spin on large modules. Same cure as the dynamic-CALLNATfix (src.sourceFile = m.sourceFilehash-join, INCLUDES-scoped for variables). Affected read constants:CALLEES,CALLERS,DIGEST/CONTEXTquery,FUNCTION_CALLERS,DB_ACCESSES, variableREADS/WRITES,*_FOR_MODULES. 2026-07-19 — fixed. Rather than thesourceFilehash-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 byReadPathBoundedTraversalIT(EXPLAIN plan asserts no unbounded CONTAINS expand incallees); full IT suite green (193/0/0). Norecreateneeded — 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. - their
-
73. A
NONE/ANYbranch 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'sVALUE_RESETclears aDECIDE's active value onNONE/ANY, so an assignment inside such a branch is attributed to the enclosing guard chain only. UnderVALUE 'TABL'→DECIDE ON #FIELD-NAME→NONE→MOVE ..., item 72 now reportsguards = [#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'sVALUEs)". This is the same complaint item 72 makes — a condition served as complete when it is not — so item 72'sguardsis 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. aDispatchGuardwithnegated=truecarrying the sibling branches' values), which is a model change, not a parser tweak. (Unmeasured: how manyNONEbranches actually contain assignments — many areIGNORE. 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
upmswith(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-ENTERisCONTAINSed by 137 modules) that enumerates all 137 asma, each differing from the target's owner. The correct test — source and target single-owner — returns 0. And it must:resolvePlaceholderTargetsfiltersph.type IN ['MODULE', 'DATA_STRUCTURE'], so aFUNCTIONplaceholder is never resolved across modules; aPERFORMto another module's subroutine stays an unresolved placeholder (6 inupms) and never becomes a cross-moduleCALLSedge. A path therefore cannot enter a module at aFUNCTIONnode, 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.) -
99.
DATA_AREA_FIELDmis-split data-area lines that carry a marker column — the field name was lost and the level invented (found and fixed 2026-07-27 while implementing item 98)Symptom. Natural data-area exports (
.lda/.pda/.gda) contain lines with a marker character between the type/length columns and the level. For those lines the parser emits an invented level and uses the marker (or type) letter as the field name; the real field name never reaches the graph at all.Cause.
DATA_AREA_FIELDis^\s*(.*?)(\d)([#A-Za-z][#\w-]*)\s*(.*)$— the prefix is non-greedy, so the regex takes the first digit followed by an identifier as the level. Normally that is right, because the prefix ends in<TYPE><spaces><LENGTH>and<LEVEL><NAME>follows directly:A 60 2##COMMAND -> prefix 'A 60', level 2, name '##COMMAND' (correct)But when a marker is glued to the length, the regex stops too early — inside the length:
A 4C 2#C-PADRE_START-PATTERN parsed : level 4, name 'C' <- '4' is the length, 'C' the constant marker correct : level 2, name '#C-PADRE_START-PATTERN' S0001A 50M 2CRITERIA parsed : level 1, name 'A' <- 'S0001' is the marker column, 'A' the type correct : level 2, name 'CRITERIA'Measured impact (whole
upmscorpus, 2726 data-area files):- 60 files affected, 931 lines mis-split.
- Of those, 333 lines in 31 files produce a phantom group at level 1.
- The invented names are nearly always marker/type letters:
C(739×, the constant marker),A(117×),I(44×),N(20×),M(9×),B,P. - The invented levels come from the length digits and range from
0to6.
Consequences.
- The real field name does not exist.
search_identifiercannot find#C-PADRE_START-PATTERN;data_structure_fieldsshows a field calledCinstead. - Group nesting collapses. The level is arbitrary, and at level
0thegroupStackis emptied completely (while peek().level() >= level) including the root — every following field in the file loses its parent. - The wrapper root disappears.
parseDataAreaonly creates the file-named root whentopLevelNamesreports exactly one top-level group. A phantom level-1 group makes it two, and a module'sUSING <area>then references a node that does not exist — exactly what happens atYLORDVL1.lda:30. - Identifier-index pollution. 739 nodes named
Cacross the corpus, which can collapse together at thesourceFile=""placeholder level.
Relation to item 98. Item 98 was not blocked by this: its enricher deliberately joins on
area.sourceFileinstead of walkingCONTAINSfrom the root, precisely because that root was not guaranteed to exist. That join stays — it is the more robust one regardless.Done. The export is column-oriented —
[<occ>] <TYPE> <LENGTH>[<marker>] <LEVEL><NAME>— where<occ>is an occurrence/superdescriptor column (S0001,0013) glued to the type and<marker>(Cconstant,Mmultiple-value,*) is glued to the length. New anchoredDATA_AREA_FIELD_EXPORTis tried first, with the permissiveDATA_AREA_FIELDkept as a fallback, so anything the anchored form does not recognise keeps its previous behaviour exactly — regression is impossible by construction.topLevelNamesuses the same split, otherwise a phantom level-1 group would still suppress the wrapper root. Deliberately a targeted grammar, not a complete one: the zero-padded two-digit level inA 8*03COD-GENAGREEis left to the fallback, which already resolves it correctly.Measured over all 2726 data areas: 59 282 lines parse identically, 931 are corrected (in 60 files), 1 338 fall back to the previous behaviour verbatim. Tests:
NaturalParserTest#dataAreaMarkerColumnsDoNotStealTheLevelAndName(one case per marker shape) and#dataAreaLinesWithoutAMarkerColumnAreUnchanged(regression guard — the source form, the export view marker and the plain field all depend on regex backtracking past the occurrence column, so they are pinned explicitly rather than assumed).Knock-on:
YLORDVL1.ldaregains a single top-level group, so its wrapper root reappears and itsUSINGreference resolves — item 98's enricher now reachesVDB2-VERSIS_LISTORDERtoo.Open. What the
*marker means is still unknown (the adjacent comment onA 1002* 2V25C6961-RECORDreads/* #01 - alte Länge, hinting at a superseded field). Both the old and the new grammar treat it as a live field, so including it changes no outcome — but if it marks a removed field, those declarations are wrong in the graph either way, which would be its own item. -
98. View aliases declared in a
USINGdata area were reported as tables (2026-07-27, follow-up to item 95). Item 95's alias pre-scan is per-module over the copycode-expanded lines, but aLOCAL USINGdata area is a separate module, so a view declared there stayed unresolved:YGEAGBNH.nat:2617doesFIND (1) VDB2-VERSIS_GENAGREEwith the view declared inYGEAGVL1.lda, sodb-accessesreported the alias instead ofVERSVW_GENAGREE. Root cause was deeper than cross-file scoping:parseDataAreadid not recognise the data-area export view marker at all. In an export a view isV 1VDB2-VERSIS_GENAGREE VERSVW_GENAGREE DA:00,00…— there is noVIEW OFtext, and theVprefix was read as a data type, so the view became aVARIABLEwith noUSES_TYPEto its DDM and no group push, letting its columns escape to the file root. 45 data areas use this form; 32 modules do DML on an alias only declared there. Done: (a)parseDataAreatreats prefixVas a view —DATA_STRUCTURE+USES_TYPEto the DDM (first token of the rest), fields now nesting under it; (b) new enrichment stepsresolve-view-alias-tables READS|WRITES+resolve-view-alias-access-nodesredirect the module'sREADS/WRITESand itsDB_ACCESSnode onto the real table, scoped by the module's ownUSINGset — a name-based redirect would be arbitrary, sinceNEXT-VIEWalone is declared over 100 different tables corpus-wide.size(reals) = 1leaves a contradictoryUSINGset unresolved rather than guessed; the join is onarea.sourceFile, notCONTAINSfrom the area root, because that root only exists when the file has one top-level group (see item 99). Tests:NaturalParserTest#dataAreaExportViewMarkerLinksToTheTableAndNestsItsFields, ITNaturalCrossFileViewAliasIT(incl. two modules resolving the same alias name to different tables); the enricher was verified load-bearing by disabling it and watching the IT go red. -
97.
call-treeleaked the dynamic-call placeholder a manual override only hides (2026-07-27, third WGEAGB0S deep API audit).call-treeforWGEAGB0Slisted#GETSHORT-MODUL— a variable (YGEAGGNH.nat:443,CALLNAT #GETSHORT-MODUL) — as aMODULEin the closure, whilecalleesfor the same module correctly reported only the resolved targetYGEAGGN0. Cause: a manual override does not delete the marker edge to the variable-named placeholder, it setsmanualHidden = trueand relies on the read queries to suppress it (DELETE_DYNAMIC_CALLNAT_PLACEHOLDER_EDGES).callees/callersfilter it; the BFS behindcall-treedid not —MODULE_HOP_OUT/MODULE_HOP_OUT_WIRINGdid 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 bindrand applycoalesce(r.manualHidden, false) = false, matchingcallees/callers. Characterization ITDynamicCallOverrideIT#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 theWGEAGB0Sclosure — everyY****MN0CRUD module — produced noWRITESedge, sodb-accessesshowed them as read-only plus a singleSTORE. Cause:DB_WRITE's(?!\()guard (added by item 90 to stop a phantom(OLD.)table) suppressed the phantom but never recovered the real table, andDELETEwas only handled in its SQLDELETE FROMform. Done: newDB_WRITE_BY_REFplus a pre-scan mapping eachFIND/READstatement 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 withNaturalCoarseScannerso tier-1 and deep agree. Tests:NaturalParserTest#updateAndDeleteByReferenceResolveToTheEnclosingLoopTable,#byReferenceWriteWithoutAResolvableLoopNamesNoTable,NaturalCoarseScannerTest, ITNaturalViewAliasDbAccessIT. A label may also introduce a SQLSELECTloop rather than aFIND(YELEMMN0,YMULTMN0hold their record that way) — those resolve through theFROMclause;#byReferenceWriteResolvesThroughALabelledSelectLoop. Verified on liveupms: all 34 by-reference sites in theWGEAGB0Sclosure now recorded, 0 missing. -
95. Natural view aliases were reported as DB tables (2026-07-27, third WGEAGB0S deep API audit).
db-accessesnamed the Natural view variable of a DML statement, not the DDM it is declared over: 58 rows across 13 of the 65 modules in theWGEAGB0Sclosure, 32 alias names standing in for 20 real tables. Worst effects — the generator's boilerplate aliasNEXT-VIEWbecame oneDB_TABLEnode shared by 11 modules meaning 11 different tables (and reporting no columns), and 11VDB2-*-VLOGaliases hid every write toVERSVW_LOGFILE, so "who writes the audit log?" answered nothing. Cause:VIEW OFwas only recognised inparseDataArea(.pdafiles);parseModule— which parses every.nat— never built an alias map, and the DML branches passed the operand verbatim todbTable(...). Done:VIEW_DECLpre-scan over the copycode-expanded lines feedsresolveViewAliasinto theDB_WRITE/DB_READbranches;dbTable()now upper-cases (Natural is case-insensitive andDB_TABLEmerges on the name). Shared withNaturalCoarseScannerso a shallow and a FULL module cannot report different names for the same statement. Tests:NaturalParserTest#viewAliasResolvesToTheUnderlyingTable,NaturalCoarseScannerTest, ITNaturalViewAliasDbAccessIT(incl. the same alias in two modules resolving to two tables). Verified on liveupms: alias rows in theWGEAGB0Sclosure 58 → 1, the phantomNEXT-VIEWnode gone,VERSVW_LOGFILEreachable for the first time. The remaining row is the cross-file case, item 98. -
94.
call-treeno longer enumerates paths;followWiringusable again (2026-07-20, JX0034N0 ↔ MultiTableImportJob functional comparison).call-tree?followWiring=truetimed out onpuratdepth ≥ 2(>120s; depth 1 already took 5.3s), which made the Java wiring closure unobtainable. Measured cause — a single quantified path pattern overCALLS|INJECTS|REFERENCES, bounded bymaxDepth × (1 + internalBudget)(= 42 at depth 2), recovering each target's depth asmin(#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 2Natural JX0034N0, depth 53 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.addWiringEdgesand the CHA steps only ever emit class-to-class, so they can never reach aFUNCTION. Fix: split the query.MODULErows now come straight from themoduleDepthsBFS (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 onlyFUNCTIONrows 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 internalPERFORMchain —DEPTHLEAFis now reported at depth 1, which also removes a standing contradiction withdb-accesses/sql-statements, whose module set always came from the same BFS.truncatedaccordingly now means "some module's internal subroutine chain may be cut off". Covered byCallTreeTruncationIT(both tests).Measured live after deploy —
call-tree?followWiring=trueonMultiTableImportJob: depth 2 1.9s (was >120s) with the same 71 modules, depth 6 3.0s, depth 10 2.3s converging at 852 modules. Onupms/JX0034N0at depth 5 the module set grew 46 → 53 with nothing lost; the seven that had been hidden areNDBERR,NDBNOERR,USIX009N,USIX052N,USIX053N,YELEMGN0,YLITEMN0. They are real:USIX052NisCALLNATed byISI173N0(line 474), itself a direct callee ofJX0034N0, so it sits at module depth 2; andYLITEMN0was already reported bydb-accesses?depth=5as avia, which is the contradiction this item removes. -
93. Transitive
db-accesseslost theDECLARESrows (2026-07-20, JX0034N0 ↔ MultiTableImportJob functional comparison).DB_ACCESSESresolves a table from three sources —READS/WRITES, an entity's ownMAPS_TO, and a repository'srepositoryEntity(item 32) — but its transitive counterpartDB_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 withdepthsilently dropped its table. Minimal repro:db-accessesonMultiTableEntryEntityreturnsmulti_table_entry/DECLARES,db-accesses?depth=1on that same module returns[]. Consequence: a Java caller's transitivedb-accessescame 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_MODULESnow carries the same three UNION branches, withvianaming the module that declares the table.SQL_STATEMENTShas no such branches, soSQL_STATEMENTS_FOR_MODULESneeded no change (verified). Covered byJavaRepositoryOwnTableIT.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 comparingcalleesagainst source:- A — inherited
INJECTS/REFERENCESlost their origin file (635× in the MTIJ closure, 255 with alineNopast the caller file's end).LINK_REFERENCES_TO_SUBCLASSES/LINK_INJECTS_TO_SUBCLASSES(item 31) copied the base class'slineNoonto the subclass edge but never setoriginFile, so thecalleessites(coalesce(r.originFile, source.sourceFile)) fell back to the subclass file. Worked example:MultiTableImportJobreportsPurBatchJobListener REFERENCES lineNo=133, but that file has 93 lines — line 133 is inAbstractPurBatchJob.java. Same as the Natural copycode bug (items 66/91), for Java inheritance. Fix: materialized edges now carryoriginFile = coalesce(r.originFile, base.sourceFile)+inheritedFrom = base.name. - B — CHA fanned constructor calls out to subtypes (48× phantom
CONSTRUCTORcallees).LINK_CALLS_TO_IMPLEMENTATIONSapplied class-hierarchy analysis tocallKind='CONSTRUCTOR'CALLS, sonew ArrayList<>()produced a phantom→ InputConstraintHolder [CONSTRUCTOR](itextends ArrayList), andnew 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(andNumberUtils/StringUtils) were resolved by simple name to the project's own same-named class → aDateUtils EXTENDS DateUtilsself-loop that also poisoned the inheritance materialization. Fix:JavaParser.supertypeNamekeeps the FQN when a qualified supertype's simple name equals the declaring class's own name; the materializers additionally guardsub <> base. The parser fix stops new self-edges, but a non-wipingrefreshleaves the old self-EXTENDSbehind (both endpoints are the surviving class node, so node reconciliation never sweeps it — the edge gap item 86 closed for Natural), so adelete-self-inheritance-edgesenrichment step reaps any self-EXTENDS/IMPLEMENTSedge project-wide before the inheritance graph is traversed. - Infra: a
delete-synthetic-inheritance-edgesenrichment step reaps allresolvedVia:'INHERITANCE'edges before the three materializers rebuild them, so a non-wipingrefreshpicks up the new properties/gates (otherwiseMERGE ... ON CREATEnever updates a pre-existing edge). Query-only fixes for A/B + reap; parser fix for C. ITJavaInheritanceWiringIT(3 tests). No response-shape change —originFileflows through the existingcalleessites.callSiteFile.
- A — inherited
-
91.
db-accesses/workfile-accesses/sql-statementscarry copycode provenance (2026-07-19, third WGEAGB0S deep API audit — manual source pass). A DB or work-file access whose statement lives in anINCLUDEd copycode was reported with a copycode-locallineNoand no file context, so the number read as a line of the host module. Concretely: the DB2 sequence readSELECT … FROM SYSIBM-SYSDUMMY1lives inUSIX043C.cpyat lines 31/39/45/51/57;db-accessesfor the 9 including modules (YAPRFMN0, YCUACMN0, YLITEMN0, YMODAMN0, YMTABMN0, YMULTMN0, YPRODMN0, YRAMOMN0, YUGRPMN0) reported those as barelineNosthat land on each host's own comment/DEFINE DATAlines. Root cause: the provenance was already on theREADS/WRITESedge (item 66 stampsoriginFile/viaCopycode/includedAton every edge inCopycodePreprocessor.remap, persisted viaSET r += e.properties), but theDB_ACCESSES/WORKFILE_ACCESSES/SQL_STATEMENTSqueries never returned it — exactly the gap item 85 closed forfunctionsand item 66 forcallees/variables. Fix (pure query + DTO, no re-parse of data):db-accessesandworkfile-accessesnow returnsites: [{lineNo, sourceFile, viaCopycode, includedAt}](newAccessSiterecord) alongside the keptlineNos;sql-statementsgainssourceFile+viaCopycodefrom the DB_ACCESS node's (remapped) file.includedAtis stored as a string, so the site queries wrap it intoInteger(...). 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. ITWorkfileAndCopycodeFunctionIT#dbAccessSiteNamesTheCopycodeFileForCopycodeSourcedAccess(hostFINDvs copycodeFIND→sites[0].sourceFile/viaCopycodedistinguish 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 sentsearch/identifier?name=&limit=25, but the query had noORDER BYand 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_IDENTIFIERgains$priorityModule— it does not filter (unlikemodule=) but computes apinRank(0 for that module's file, else 1) andORDER BY pinRank, sourceFile, startLine, so the local match survives thelimitwhile the global list is preserved; ordering is now deterministic (it was undefined before). Delivered across REST (priorityModule), MCPsearch_identifier, CLI--priority-module, and the UI hook. ITIdentifierPriorityModuleIT(four modules sharing one LOCAL field,PRIO_ZZZ_TARGETsorts last: excluded atlimit=2without the pin, first in the page with it, and the full set still returned at a large limit — i.e. no filtering). -
90.
DELETEno longer mis-parsed as a table write (2026-07-19, second WGEAGB0S deep API audit, Finding 5). Natural DMLDELETE [(label)]deletes the current record of the enclosing READ/FIND loop and names no view, and theEXAMINE … DELETE [FIRST]clause is not a DELETE statement at all — butDB_WRITEcaptured the token afterDELETEas a table, producing phantomFROM(108×, from SQLDELETE FROM <table>),(OLD.)/(*)/label refs (19×), andFIRST(7×) — and, for SQL, lost the real table (it sat afterFROM). Fix:DELETEremoved fromDB_WRITE(STORE/UPDATE keep their view operand); a newDB_DELETE_FROMcaptures the SQLDELETE FROM <table>real table (modeDELETE);DELETE (label)/DELETE FIRSTname no table. The same label-reference shape also affectsUPDATE (label)(UPDATE (OLD.)/(HOLD-PRIME.)) and a(*)read operand — a view never starts with(, soDB_WRITE/DB_READnow carry a(?!\\()guard that rejects a parenthesized label reference (no phantom(OLD.)/(*)table) whileUPDATE <view>still records the real view. Both parsers. Tests inNaturalParserTest(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, notDB_READ. A SELECT column list can contain a hyphenated Natural field whose last segment is literallyFROM(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 realFROM VERSVW_COMISIONclause below. Fix:FROM_VIEWnow uses a negative lookbehind(?<![-\\w.])FROM(FROM must be a standalone SQL keyword, not an identifier tail) plus the(?!\\()operand guard. Both parsers. TestNaturalParserTest#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 matchedREAD WORK FILE; the corpus also writesREAD WORK 1 ONCE RECORD …(289× project-wide) withoutFILE, which still fell through toDB_READand produced a phantomDB_TABLE 'WORK'(154 accesses). Fix: theWORK [FILE] nguard and theWORKFILE_ACCESS/WORKFILE_DEFINEpatterns now treatFILEas optional (guarded on a following digit, so a view whose name merely starts withWORKis unaffected). Both parsers;NaturalParserTest(READ WORK 1 ONCE RECORD). -
88. Finalize sweep deletes edgeless
DB_TABLE/WORKFILEplaceholder 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 todb-accesses(edge-driven) but still surfacing insearch_identifier?type=DB_TABLEand the DB-table inventory (observed: orphanedNUMBER/WORK/FIRSTafter items 84/87). New finalize stepdelete-orphaned-placeholder-tables(DELETE_ORPHANED_PLACEHOLDER_TABLES) removes anyDB_TABLE/WORKFILEwith no relationships; degree-0 only, so a table any file still accesses (or a Java@Entity'sMAPS_TOtarget) is kept. Covered byStaleTableEdgeReapIT.orphanedPlaceholderTableNodeIsDeleted. -
87.
FIND NUMBER <view>no longer mis-parsed as a phantomDB_TABLE 'NUMBER'(2026-07-19, second WGEAGB0S deep API audit).FIND NUMBER <view>is a count-only FIND (natural-grammar.md §7.1);NUMBERis a statement keyword, not the accessed view — but theDB_READregex (in bothNaturalParserandNaturalCoarseScanner) captured it as the table name, sodb-accessesreported a bogusNUMBERtable 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; 182FIND NUMBERoccurrences project-wide). Same class as item 84'sREAD WORK FILE. Fix:DB_READnow skips the FIND optionsALL/FIRST/NUMBER/UNIQUEand theRECORDS/IN/FILEnoise words before the view (and allows a variable record-limit(operand), not just a literal). Covered byNaturalParserTest(FIND NUMBER MY_VIEW/FIND NUMBER IN FILE OTHER_VIEW). Item 86 reaps the existingNUMBERedges on the next refresh. -
86. Re-ingest reaps stale Natural
DB_TABLE/WORKFILEaccess edges (self-healing) (2026-07-19, follow-up to items 84/85). A statement whose access target changed between parses orphaned its oldREADS/WRITESedge 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 theWORKdb-access that lingered onUSIX052Nafter the item-84 parser fix (a pre-fixREAD 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_EDGESdrops itsREADS/WRITESedges toDB_TABLE/WORKFILEplaceholders; the fresh parse (which always re-emits them) re-creates the current ones, unchanged ones round-trip identically. Scoped tolanguage:'natural'source nodes — Java DB edges are resolver-built (RESOLVE_JAVA_DB_ACCESS) and untouched. Covered byStaleTableEdgeReapIT(editREAD VERSVW_OLD→READ VERSVW_NEW+READ WORK FILE, assert old view +WORKgone). (A clean re-ingest ofupmsalready cleared the existing staleWORK; item 86 prevents recurrence on incremental refreshes.) -
84. Natural work-file access tracking (
workfile-accesses) +READ WORK FILEno 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 bothNaturalParserandNaturalCoarseScanner, creating a bogusDB_TABLE 'WORK'READS access (seen onUSIX052Nin the WGEAGB0S call tree). BothDB_READpatterns now negative-lookaheadWORK FILE, andREAD/WRITE WORK FILEare modelled as first-classWORKFILE+WORKFILE_ACCESSnodes (analogue ofDB_TABLE/DB_ACCESS), keyed by work-file number, with the record buffer on theREADS/WRITESedge and theDEFINE WORK FILE n '<name>'physical name on the node. NewGET /modules/{name}/workfile-accesses→[{workFile, physicalName, mode, recordBuffers, lineNos}], MCPworkfile_accesses, CLIac workfile-accesses. Covered byNaturalParserTest(READ + WRITE) and full-stackWorkfileAndCopycodeFunctionIT;mcp-api-usage/system-prompt docs updated. -
85.
/functionsitems carrysourceFile+viaCopycode(copycode-provided subroutine provenance) (2026-07-19, WGEAGB0S deep API audit, Finding 1). A subroutine pulled into a module viaINCLUDEwas listed withdeclaredIn=the including module and the copycode'sstartLine/endLinebut no file, so the lines pointed outside the module's own (shorter) file — e.g.ISIN0019(59-line file) reportedGET-FORMATat 90–120, which actually live inISIC0010.cpy. The FUNCTION node already stored the rightsourceFile; theMODULE_FUNCTIONS/MODULE_FUNCTIONS_OWN/_INHERITEDprojections just dropped it. NowInheritedFunction/FunctionInfoexposesourceFile(+ derivedviaCopycode = f.sourceFile <> m.sourceFile). Covered byWorkfileAndCopycodeFunctionIT; docs updated. -
Module
callersdefault is external-only; noMODULEself-loop (2026-07-19, WGEAGB0S deep API audit). Two coupled defects inCypherQueries.callers(scope): (1) the top-level main body'sPERFORMs originate at theMODULEnode, soscope=internal/default reported the module as its own caller (WGEAGB0S → WGEAGB0S), a self-loopcalleesnever mirrors — fixed withAND caller <> mon 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 toexternal(genuine incoming CALLNAT/inheritance only).context/digest(both callcallers(…, null)) inherit the clean view;scope=internalstill exposes function→function PERFORM wiring; callees unchanged. Covered byModuleCallersSelfLoopIT(fails 2/3 before the fix). MCPcallerstool description + REST endpoint doc +mcp-api-usage-ac-implementation.mdupdated. (Supersedes the earlier "Not a bug (verified):context.callersincludes internal PERFORM callers — noisy but accurate" note.) Ego-graphdirection=inforWGEAGB0Snow returns its dynamic callersW-LST-N0/W-MNT-N0(item 75). Payloaddirectionis alwaysREQUESTfor 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
FUNCTIONnode (a subroutine/method) whenever the call originated inside a subroutine rather than the main body —ModuleCallersSelfLoopITmissed it because its fixture callerCALLNATs from the main body, where the edge already starts at theMODULE. Measured onupms: 46/56 modules in theWGEAGB0Sclosure hadFUNCTION-typed rows in the defaultcallers, 29/56 had duplicate rows, and hot utilities were unusable (CDRANGEdefaultcallers= 500 rows / 3 distinct FUNCTION names / 0 module callers). Fixed by making the external branch ofCypherQueries.callers(scope)roll every caller up to its owningMODULEvia(callerModule:MODULE)-[:CONTAINS*0..1]->(source)-[r]->(m)andcollect(DISTINCT …)— symmetric with howcalleesanchors its source side. Covered by newCallersRollupIT(caller invokes from inside a subroutine, twice → one rolled-up MODULE row with two aggregated sites, no FUNCTION leak); the oldModuleCallersSelfLoopIT,JavaWiringIT,JavaModulesExtendsFilterITstay green.
- 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
-
100.
DEFINE DATA ... USING <member>binds by level-1 record name, not by member (file) name (found and fixed 2026-07-28, WGEAGB0S deep API audit tier 2 — 19 of 379USINGsites (5.0%) in the WGEAGB0S call-tree closure are wrong or unresolved)Symptom, two shapes.
- Wrong file.
GET /api/projects/upms/modules/WGEAGB0S/data-structuresreportsGround truth:W-WIF-A2 USING PDA fieldCount=10 src/manual/parameter_data_area/old/W-WIF-A7.pdaWGEAGB0S.nat:47saysPARAMETER USING W-WIF-A2, i.e. memberW-WIF-A2=new/W-WIF-A2.pda(5 fields:P-LINE-TYPE/LEVEL/KEY/VALUE) — exactly the fields the module uses at 386, 732–735 and 1286.old/W-WIF-A7.pdais a different member whose level-1 record was copy-pasted as1W-WIF-A2; it holdsP-REST-*, whichWGEAGB0Sreads fromW-WIF-A1(verified:W-WIF-A1carriesP-REST-FLAG,P-REST-LEVEL-IND,P-REST-POINT-KEY,P-LINE-START,P-LINE-END). So bothsourceFileandfieldCountare wrong. Same shape:BGEAGFN0/USIX052NUSING YFRAMBL0→old/ZFRAMBL0.ldainstead ofnew/YFRAMBL0.lda, andUSIX052NUSING YFRAMBL1→new/ZFRAMBL1.ldainstead ofold/YFRAMBL1.lda. Not cosmetic:YFRAMBL0/ZFRAMBL0andYFRAMBL1/ZFRAMBL1differ in the browse-array boundV(CONST<13>vsCONST<1000>), so an agent reading the wrong twin gets the wrong page size. - Never resolved at all.
USING VLAYERLA,USING USIX020L,USING USIX036LreportsourceFile: null, area: UNKNOWN, fieldCount: 0— although all three.ldafiles exist inside the project root and are ingested. 15 of the 19 affected sites are this shape (10×VLAYERLAinISI173N0/VMULTDN1/VMULTGN1/VMULTMN1..4/VMULTON1/VMULTSN2/ZINELEM1, 2×USIX020LinISI173N0/USIX021N, 3×USIX036LinYGARAMN0/YMODAMN0/YPRODMN0).search/identifiershows only thesourceFile: ""placeholder for each.
Cause, two cooperating places.
NaturalParser.parseDataArea(ac-parser-natural/.../NaturalParser.java, ~line 920) emits the member-named wrapper root only for the single-top-level case:A data area with several level-1 records therefore gets no node named after its member, so aif (topNames.size() == 1 && !topNames.get(0).equalsIgnoreCase(areaName)) { … }USINGof it can never resolve.VLAYERLA.lda(constants),USIX020L.lda(1#C-HM-FUNC, …) andUSIX036L.lda(1#V-ID-TRAN_TAB_FWD, …) are exactly that case. The existing comment claims "multi-top-group areas keep their existing shape (no wrapper, no name collision)" — that decision is what produces shape 2.CypherQueries.resolvePlaceholderTargets(ac-neo4j-store/.../CypherQueries.java, ~line 2650) matches a placeholder purely on(type, name, project). It already excludes module-owned groups (item 74) but has no preference for the node that is the member root of a data-area file, and no tie-break when two files declare the same level-1 name — soUSING W-WIF-A2matches the level-1 node insideW-WIF-A7.pdajust as well as the root ofW-WIF-A2.pda. Inupms42 level-1 names are declared in more than one data-area file, so this is not a one-off.
Fix. (a) In
parseDataArea, emit the member-named root whenever no level-1 record already carries the member name (!topNames.contains(areaName)), parenting every level-1 record under it — so each data-area file contributes exactly one node named after its member. (b) InresolvePlaceholderTargets, forDATA_STRUCTUREplaceholders prefer arealnode that is a data-area member root (real.sourceFileends.lda/.pda/.gdaand its basename equalsreal.name), falling back to the current global name match only when no such candidate exists — so coverage never regresses forUSINGs that have no matching file. Characterization tests:NaturalParserTestcase for a multi-top-level.lda(must yield a member-named root containing all top-level records), plus a Testcontainers IT with two fixture PDAs declaring the same level-1 name (theUSINGmust bind to the file whose member name matches).Done. Both halves landed as described.
NaturalParserTest.multiTopLevelDataAreaStillGetsAMemberNamedRoot…dataAreaWhoseTopLevelAlreadyMatchesTheMemberKeepsItsShapecover the parser;DataAreaMemberResolutionITcovers the graph end to end (DAOTHER.pdacarries a copy-pasted1DAMEMBERrecord,DAMULTI.ldahas several level-1 records and none named after the member).parsesLdaWithMultipleTopLevelStructureswas updated: its "top-level structures have noCONTAINSparent" assertion encoded exactly the behaviour this item changes.
- Wrong file.
-
101.
/data-structures/{name}/fieldssilently unions homonymous definitions from different files (found and fixed 2026-07-28, WGEAGB0S deep API audit)Symptom.
GET /api/projects/upms/data-structures/W-WIF-A2/fieldsreturns 15 fields — the union ofnew/W-WIF-A2.pda(5) andold/W-WIF-A7.pda(10) — with nosourceFileon any row and no parameter to disambiguate. An agent cannot tell that it is looking at two unrelated record layouts merged into one, and will happily "verify" a field that the module it is analysing cannot see.Cause.
CypherQueries.DATA_STRUCTURE_FIELDScollects every same-named definition intocanonandUNWINDs it:MATCH (s0:AstNode {type: 'DATA_STRUCTURE', name: $name, project: $project}) WITH collect(s0) AS defs WITH [d IN defs WHERE d.sourceFile <> ''] AS withFile, defs WITH CASE WHEN size(withFile) > 0 THEN withFile ELSE defs END AS canon UNWIND canon AS sThe Javadoc and
agent-api-system-prompt.mdboth claim the opposite ("scoped to the structure's own definition"), so the contract is documented as something the query does not do.Fix. Return
sourceFileon every row, accept an optional?sourceFile=filter, and when the parameter is absent and more than one definition exists prefer the member-root definition (item 100) rather than the union. Covered by an IT with two fixture areas declaring the same level-1 name.Done.
DataStructureFieldgainedsourceFile(sodb-tables/{name}/columns, which shares the record, returns it too); REST?sourceFile=, MCPdata_structure_fields(sourceFile)and CLIac data-structure-fields --source-filedelivered together. Covered byDataAreaMemberResolutionIT.dataStructureFieldsAreNotUnionedAcrossHomonymousDefinitions(fails before the fix: the decoy record's fields are merged in) and…CanBePinnedToOneSourceFile. -
102.
module_data_structurescollapses homonyms into one row with an arbitrary file and a blendedfieldCount(found and fixed 2026-07-28, WGEAGB0S deep API audit)Symptom. The
W-WIF-A2row onWGEAGB0SreadssourceFile = old/W-WIF-A7.pda, fieldCount = 10— a combination that is wrong even if you accept either candidate as the intended one, because the file and the count can come from different nodes.Cause.
CypherQueries.MODULE_DATA_STRUCTURESgroups byd.nameonly and then reportsWITH d.name AS name, rel, max(fc) AS fieldCount, head([sf IN collect(d.sourceFile) WHERE sf <> '']) AS sourceFilehead(collect(…))is order-dependent (nondeterministic across ingests) andmax(fc)is taken over all homonyms, so the reportedsourceFileandfieldCountneed not describe the same definition.Fix. Group by
(name, sourceFile)and return one row per resolved definition. With item 100 in place this normally collapses back to a single row; where it does not, the agent sees the ambiguity instead of a fabricated blend. Covered by the same IT as item 101.Done. A still-unresolved placeholder is reported only when no resolved definition exists for that
(name, relationship), so thesourceFile: null/area: UNKNOWNrow keeps its meaning. Covered byDataAreaMemberResolutionIT.moduleDataStructuresReportsOneRowPerUsedDefinition(before the fix:[DAMEMBER.pda, DAOTHER.pda]collapsed to one arbitrary row). -
103.
db-accessessilently truncates at the defaultlimit=50— a bare array with notruncatedsignal (found and fixed 2026-07-28, WGEAGB0S deep API audit)Symptom. Measured on
upms:GET /modules/WGEAGB0S/db-accesses?depth=10 → 50 rows GET /modules/WGEAGB0S/db-accesses?depth=10&limit=500 → 64 rowsThe 14 dropped rows hide 7 tables entirely —
VERSVW_MULTILIN,VERSVW_MULTTABL,VERSVW_PRODUCTO,VERSVW_RAMO,VERSVW_TABLAS,VERSVW_USERGRP,VERSVW_USUARIO_NEW. The response is a bare JSON array: no envelope, no total, notruncatedflag, so the caller cannot detect the cut. An agent following the documented Natural playbook (db-accesses?depth=3, nolimit) therefore gets a silently incomplete DB footprint — the single most damaging failure mode for a reengineering or impact analysis, because it looks like a complete answer.Cause.
AnalysisResource.dbAccessesapplieseffectiveLimit(limit), which defaults to 50.sql-statementson the same closure has no such cap (389 rows returned uncapped), so the two endpoints disagree about the same data.workfile-accessesshares the defaulted limit.Fix. Remove the default cap on
db-accesses/workfile-accessesso they matchsql-statements.Done — narrower than first proposed. Only the default cap was removed (
AnalysisResource.uncappedLimit- the same helper in
McpQueryTools); the{items, total, truncated}envelope was not added. Rationale: the defect is silent truncation, i.e. a cut the caller never asked for and cannot detect. An explicitlimitis neither — the caller chose it — so wrapping the response would change the array contract for every REST/MCP/CLI/UI consumer to signal something already known. If atruncatedflag is wanted anyway, it should be a separate item covering all paginated endpoints, not just these two. Covered byIncludeProvenanceAndAccessLimitIT.dbAccessesAreNotSilentlyTruncatedAtFifty(60-table fixture; returns 50 before the fix) and…explicitLimitIsStillHonoured.
- the same helper in
-
104.
includedAtis ambiguous for nested copycode includes — it can point into an intermediate file that the response never names (found and fixed 2026-07-28, WGEAGB0S deep API audit)Symptom.
GET /modules/ISI173N0/calleesreports theYFRAMN04callee asviaCopycode: 'YFRAMC01', includedAt: 27. Line 27 ofISI173N0.natis a comment. The real chain isISI173N0.nat:232 INCLUDE USIX050C 'YFRAMMC1' … USIX050C.cpy:58 INCLUDE &1& (parameterised) YFRAMMC1.cpy:27 INCLUDE YFRAMC01 YFRAMC01.cpy:12 CALLNAT 'YFRAMN04'includedAt: 27is a line inYFRAMMC1.cpy— an intermediate file that appears nowhere in the response. In the single-level case (WGEAGB0S → ADLML02,includedAt: 673= theINCLUDE ISIYESNOstatement) the same field is a line in the module's own file, so the field silently means two different things and the caller cannot tell which. (The resolution itself is correct — the parameterised 3-level expansion is followed properly; only the provenance reporting loses the chain.)Fix. Report the chain rather than one line: make
includedAtalways the line in the module's own file (232 here) and add a fullincludePath: [{sourceFile, lineNo}, …]on thesitesentries ofcallees/callers/db-accesses/workfile-accesses. Covered by an IT over a 2-level include fixture.Done.
CopycodePreprocessor.LineOriginnow carries the host include line unchanged through every nesting level plus the chain as a compactfile:line>file:lineedge property (Neo4j properties cannot hold a list of maps);CallSite/AccessSiteexpose it asList<IncludeStep>.sitesis a nested response object, so MCP and the CLI pick the new field up without a signature change. Covered byIncludeProvenanceAndAccessLimitIT.nestedIncludeReportsHostLineAndTheWholeChain(before the fix:includedAt = 2, the line inside the intermediate.cpy— theISI173N0shape exactly) and…directCallHasNoIncludeChain. -
106. A module's resolved
USINGedges are never reaped, so an old binding survives every refresh (found and fixed 2026-07-28 while re-verifying item 100 against the liveupmsgraph)Symptom. After the item-100 fix had landed and
upmshad been rebuilt and deep-refreshed,WGEAGB0S USING W-WIF-A2reported two rows — the correctnew/W-WIF-A2.pda(5 fields) and the pre-fixold/W-WIF-A7.pda(10 fields). The fix had not failed: the correct edge was created, the wrong one simply was never removed. Measured across the WGEAGB0S closure: the 19 wrong/unresolvedUSINGsites dropped to 5, and all 5 residuals were this shape — a stale edge sitting next to the right one (BGEAGFN0/USIX052N→YFRAMBL0,USIX052N→YFRAMBL1×2,WGEAGB0S→W-WIF-A2).Cause. Item 86 reaps a re-parsed Natural file's stale access edges, but only those pointing at a placeholder (
sourceFile = ""). AUSINGedge is resolved onto a realDATA_STRUCTUREnode byresolvePlaceholderTargets, and from then on nothing deletes it:MERGEonly ever adds. So any binding an older ingest made is permanent. This is not specific to item 100 — plain editing of a module'sDEFINE DATA ... USINGlist leaves the dropped data area attached forever, which is the more common everyday case.Fix.
DELETE_STALE_NATURAL_USING_EDGES, theINCLUDEScounterpart of item 86, run in the same spot (right before the fresh edges are merged). Deleting all of a re-parsed file'sUSINGedges is safe because the fresh parse always re-emits every one of them as a placeholder and the same finalize re-resolves them; unchanged ones round-trip identically. Covered byDataAreaMemberResolutionIT.anEditedUsingDropsTheOldBindingOnRefresh(editUSING DAMEMBER→USING DAOTHER, refresh, assert the old binding is gone — fails before the fix). Ordered last in that class because it mutates the fixture.Note: the fix prevents recurrence; it does not retro-clean a graph that already carries such edges — those disappear on the next refresh of each affected file, since the reap runs per re-parsed file.
upmsstill carried the 5 residuals at the time of writing and needs one more refresh. -
105. A fan-out query that surfaces a data-area file re-ingests it on every call —
search/identifiertook ~60-75 s per lookup (found 2026-07-28 WGEAGB0S deep API audit, root-caused and fixed the same day)Symptom.
GET /search/identifier?name=Xonupms:ZFRAMBL0(1 hit) 56.8 s / 62.2 s / 74.7 s across runs,YFRAMBL1(9 hits) 58.2 s. Both the system prompt and the Natural playbook recommend this endpoint for orientation and for confirming a candidate is a real module; at that latency it cannot be used in a loop, and a batch of lookups exceeds a 2-minute client timeout.Cause — not the query. The first suspicion recorded here (a missing/unusable
(project, name)index) was wrong, and the measurement that settled it is worth keeping:call result ?name=NOSUCHNAME12345(0 hits)0.90 s ?name=ZFRAMBL0(1 hit, a.lda)74.7 s Identical scan work, 80× the latency — so the cost is not the scan. (The scan is a full label scan:
PROFILEshows 2,000,905 DbHits, because the name predicate is wrapped in aCASEthat strips a leading Natural sigil and so cannot useast_node_project_name. But that is ~1 s, and it is the same ~1 s in both rows above. Worth its own item if 1 s ever matters; it is not this bug.)The real cost is in
withFanoutWarm→DeepIngestCoordinator.ensureDeepMany, which deep-ingests the source files a result set surfaced.FULLY_INGESTED_SOURCE_FILESasks for aMODULEnode withingestDepth = 'FULL'— but a Natural data area (.lda/.pda/.gda) produces onlyDATA_STRUCTUREnodes, never aMODULE. So a data area can never be reported as fully ingested, is treated as pending on every call, and is re-warmed forever. Server log for one lookup:07:11:29,595 Ingesting DATA_STRUCTURE ZFRAMBL0, YFRAMBL0 ... [old/ZFRAMBL0.lda] <- 40 ms 07:11:29,635 Finalizing project 'upms' (1 files persisted, scoped deep to 0 modules) 07:11:29,635 Finalize upms (scoped-deep): 45 steps 07:12:31,175 <next request> <- ~60 s in finalizeThe warm itself is trivial; each one drags a whole-project 45-step finalize behind it and then reports "changed", so the caller re-runs its query on top. A repeat lookup re-ingested the same file again — it never converges.
Fix.
DeepIngestCoordinator.ensureDeepManyfilters.lda/.pda/.gdaout of the warm candidate set (isWarmable). Nothing is lost: a data area has no deep tier — both ingest tiers run the sameparseDataArea— so warming one can never add anything to the graph. Applies to everywithFanoutWarmcaller (search/identifier,callers,callees,call-tree), not just this endpoint.Covered by
DataAreaMemberResolutionIT.surfacingADataAreaDoesNotReIngestItOnEveryCall, which asserts the nodeidis stable across two lookups — ids are regenerated on every re-ingest, so a changed id is the re-ingest. It fails before the fix with two different UUIDs. Timing is deliberately not asserted: on a small fixture the finalize is fast, so a wall-clock bound would not reproduce the bug.Note: two further latency questions were surfaced by this and left open on purpose, not folded in: the 2M-DbHit label scan above, and why a scoped 1-file finalize runs all 45 project-wide steps (
scoped deep to 0 modules). The second is the larger prize and affects every incremental ingest.
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.)
-
108.
dispatch-tableonly understands theDECIDEdispatcher, not the dispatch-table idiom — the one the endpoint is named after (found 2026-08-02,upmswebservice-layer audit)Symptom. Two dispatcher idioms are common in
upms, and the endpoint covers one of them.GET /modules/WSUBPX0S/dispatch-table → 25 rows (DECIDE ON VALUE 'supl_fin' → #W-ACT-PROG := 'WNSUPD0S') GET /modules/WPOLIX0S/dispatch-table → [] GET /modules/WGARCX0S/dispatch-table → [] (likewise WACOMX0S, WCLAIX0S, WOBJPX0S)The five empty ones dispatch through an array built from literals:
DEFINE SUBROUTINE INIT-OBJECT-TABLE ASSIGN #WT-OBJ-PROG (1) = 'WXSPOD0S' ASSIGN #WT-OBJ-PROG (2) = 'WXSDAD0S' … … #W-ACT-PROG := #WT-OBJ-PROG (#I-OBJ) CALLNAT #W-ACT-PROG …Every target is a literal in the source; nothing is runtime-dependent. These are 10 of the 116 entries in
dynamic-calls/unresolved, all withvariable: #W-ACT-PROG.Relation to item 83. Item 83 constant-folds
MOVE '<lit>'/MOVE '<lit>' TO SUBSTR(var,pos,len)chains. This is the same class — literal assignment feeding aCALLNAT var— but through an indexed array element rather than a scalar, so the fold does not apply. The set of possible targets is exactly the set of literals assigned to any element of that array; which index is live at runtime is not statically known, so the honest model is n candidate edges (as item 82 already allows for a manual override with multiple targets), not one.Fix (proposal). Extend the fold to indexed writes: collect
ASSIGN <array>(<n>) = '<literal>'for the array feedingCALLNAT, and emit oneCALLSedge per literal that names a real ingested module, taggedfolded=true+ something likeviaTable=true. Independently,dispatch-tableshould report them, keyed by index instead of by guard value — the response already hasguardValue/assignedValue, soguardValue: "(3)"or a dedicatedindexfield would fit. Caveat: inupmsmost of these targets are not ingested (see item 107), so the edges would resolve to nothing there — the value is in no longer silently reporting[]. -
109.
variables/{name}/writesgives the location but not the written value, so resolving a dispatch needs the source anyway (found 2026-08-02,upmswebservice-layer audit)Symptom. Working around item 108:
GET /variables/%23WT-OBJ-PROG/writes?module=WPOLIX0S → [{"function":"INIT-OBJECT-TABLE","sourceFile":"…/WPOLIX0S.nat","lineNo":772, …}, … 6 rows]Six correct write sites, and not one of the six assigned values. Answering "what does this dispatcher dispatch to" therefore requires opening the file and reading lines 772/775/777/780/783/785 — the API narrows the search to the right lines and then stops one step short.
dispatch-tablealready returnsassignedValuefor theDECIDEidiom, so the concept and the field name exist.Fix. Add
assignedValue(and, where the write is indexed,assignedIndex) to thewritesrows, populated when the right-hand side is a literal,nullotherwise. Cheap next to item 108 and useful far beyond it: "which constants does this module put into field X" is a routine question in a reengineering pass. -
110. No reachability query — "can A reach B?" has to be hand-rolled as ~100
callerscalls (found 2026-08-02,upmswebservice-layer audit)Symptom. The question was "does any
W*module reach the commission calculation (ISINCOMI/VCOMIN00/VVERAN50/VCOMIN55/VCOMIN57/VCOMIN50)?" — a yes/no with a witness path. There is no endpoint for it.call-treegoes downward from one root and returns a flat closure without paths, so it answers "what does A reach", never "who reaches B", and never "how". The workaround was a client-side breadth-first search upward over/callers, six seeds, depth 6: ~100 HTTP round-trips, 101 modules visited, and the path reconstruction written by hand.Fix (proposal).
GET /modules/{name}/reaches?target=<name>&direction=up|down&depth=Nreturning{reachable: bool, paths: [[module, …], …], truncated: bool}— or, more useful for this shape of question, a filtered variant ofcallers/call-treethat accepts a set of targets and returns only the witnesses. In Cypher this is one boundedshortestPath/variable-length match; done client-side it is 100 requests and an easy place to introduce a bug. Note the traversal must be bounded — see item 75 on theCONTAINScycles.Why it matters. "Who can trigger X" is the recurring question in legacy reengineering: which entry points reach a calculation, a table write, an external interface. It is the natural counterpart to
call-treeand currently the biggest hole in the query surface for that work. -
82. Manual override for unresolvable dynamic
CALLNATtargets (human/agent-settable) (proposed + implemented 2026-07-19, from the WGEAGB0S deep-API audit; REST + MCP +acCLI + Testcontainers ITs green). The dynamic-CALLNATresolvers cannot follow every name-assembly pattern — e.g.YGEAGGNH.nat:443 CALLNAT #GETSHORT-MODULwhere the name is built viaMOVE 'YGEAGKEY' TO #GETSHORT-MODUL+MOVE 'GN0' TO SUBSTR(#GETSHORT-MODUL,6,3)→YGEAGGN0(a real, ingested module). Such a call site leaves anunresolvedplaceholder (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 realCALLScallKind=CALLNAT_DYNAMICedge, provenancemanual). 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/digestshould also surface theunresolvedflag thatgraphalready exposes. -
83. Auto-resolver for string-assembled dynamic
CALLNATtargets (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. theY…GNH"GetShort" family (~44 modules) —MOVE 'YxxxxKEY' TO #GETSHORT-MODUL+MOVE 'GN0' TO SUBSTR(#GETSHORT-MODUL,6,3)→YxxxxGN0— plus similar families (PXFRAA01.ST-PGMacross theP…MP0programs, theYFRAMBCK.cpy:27group). Add an enricher that constant-folds a chain ofMOVE <literal>andMOVE <literal> TO SUBSTR(var,pos,len)assignments feeding aCALLNAT varinto 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 (fewerunresolvedsites; resolvedcallees/callers). Done:NaturalParserrecordsMOVE '<lit>' TO SUBSTR(var,pos,len)as aWRITESon the base var carryingsubstrPos/substrLen; newRESOLVE_DYNAMIC_CALLNAT_FOLD(+_SCOPED) folds base literal + ordered overlays (reduce/left/substring) → resolvedCALLSedge taggedfolded=true; the direct-literal/indirect/cross resolvers now skip partial-slice writes (substrPos IS NULL). Precedence honoured by guarding the fold against:DynamicCallOverridesites plus adelete-folded-overridden-dynamic-callnatstep beforeapply-manual. Characterization ITs inDynamicCallnatFoldIT(fold resolvesYABALKEY+GN0@6/3→YABALGN0; manual override wins); full dynamic-callnat regression 89/89 green. Docs inmcp-api-usage-ac-implementation.md. -
26. MCP session reliability — RESOLVED BY REMOVAL (2026-08-04) (investigated 2026-07-07, reproduced 2026-08-02, never fixed).
mcp__agenticcode__*calls intermittently — and in the 2026-08-02 session, from the very first call — failed with"the first message from the client must be initialize: tools/call", forcing every playbook step to be re-expressed ascurl. The evidence pointed at the MCP client's reconnect handling rather than a server-side bug this codebase's config could fix, and the REST endpoints answered normally throughout. Decision 2026-08-04: the MCP server surface was removed entirely rather than debugged — see "MCP surface removed" inx-docs/features.md. REST +acCLI are now the only access paths; all 40 former tools had a REST twin, so no capability was lost.