feat(#39): structural call-graph reasoning (ATLAS_CALL_GRAPH)#125
Conversation
…ATLAS_CALL_GRAPH) Adds a precomputed, cached call graph and rewires the structural veto to use it, behind ATLAS_CALL_GRAPH (default off, strictly additive). This is the deeper solver-backed layer that issue itigges22#39 asked for but that V3.1 never landed: there was no stored call graph, no transitive reachability, and the veto was a shape check rather than real resolution. The graph engine in v3-service/graph/ is a faithful Python port of the chiasmus call-graph engine. It extracts defines, calls, imports, exports, and contains facts from tree-sitter, resolves Python imports across files, and answers callers, callees, reachability, path, impact, cycles, dead-code, and entry-points natively in linear time. There is no solver: like chiasmus, the everyday queries are native graph traversal, and Prolog facts are emitted (facts.py) only so an optional solver layer can be added later without re-plumbing. A per-file content-hash cache makes recompute incremental. The port is pinned to the reference, not just plausible. Golden tests compare against chiasmus's own extractGraph and native-analyses, captured via npx tsx: extraction of a fixture with classes, methods, decorators, comprehension calls, and aliased and relative imports matches exactly, and the analyses match including path tie-breaks and impact ordering. Phase 1 deepens the structural veto. graph/resolve_calls.py resolves a candidate's direct-identifier calls against the import graph. A call resolves if its name is bound anywhere in the file, is a builtin, is an imported name, or is supplied by a wildcard import whose module's actual exports are resolved through the graph. The deepening over the shipped veto is that a bare call to a name that merely exists in some unimported project file is now flagged, which catches broken cross-file references. It stays conservative: attribute and method calls are never flagged, an opaque stdlib wildcard keeps it lenient, and the veto never empties the candidate set. It is wired into the pipeline after the existing structural veto and gated by the flag. Two rounds of review ran against this. The first fixed cross-request cache corruption, set-iteration nondeterminism in the analyses, an escape_atom trailing-newline case, and an unbounded-recursion gap. The second caught the real blockers in the Phase 1 veto: assigned callables and function parameters were being flagged as unresolved because the resolution set only had def and class names. That is fixed by resolving against all names bound anywhere in the file, with regression tests for callbacks, higher-order functions, and loop, with, walrus, and comprehension targets. The graph package ships in the v3-service image, CI installs the tree-sitter grammars and runs the suite, and the design and phase status live in docs/reports/CALL_GRAPH_REASONING_V3.md. Tests: 68 under tests/v3-service, all green. Credit to Dmitri Sotnikov (@yogthos) for chiasmus, which itigges22#39 cites.
Completes the structural call-graph feature end to end on top of the phase 0/1 substrate, all behind ATLAS_CALL_GRAPH and strictly additive. Phase 2 deepens the repair context. graph/context.py repair_context builds a real reachability slice for the failing function: the call path from an entry point down to it, its transitive impact set, and its callees, bounded for the token budget. It is wired into the phase-3 repair block, preferring the graph block and falling back to the shipped 1-hop call_chain_context on flag-off or any failure. Phase 3 deepens context injection. symbol_neighborhood returns a named symbol's callers, callees, impact set, and defining files, attached to the /internal/symbol_index response as an additive graph field so the matched and skipped shape is unchanged for flag-off callers. Phase 4 adds the optional tier signal. analyses.complexity reports per-node fan-in and fan-out plus the graph maxima, exposed via the complexity analysis. Wiring it into the Go tier classifier is intentionally left out, since point 2 already ships through cyclomatic complexity and the plan gates this on a tier- accuracy measurement that needs the live stack. Phase 5 adds the solver layer without a heavy dependency. The Prolog facts already emit from phase 0 for an external SWI-Prolog or chiasmus_verify. graph/datalog.py adds a compact in-process Datalog engine, facts plus structured rules evaluated to a bounded fixpoint, shipping the built-in transitive reaches closure and supporting arbitrary in-process rules. Its reaches is cross-checked against the native reachability for all node pairs in the suite, so the solver layer is provably consistent with the native one. Phase 6 adds JavaScript. extract.py dispatches by extension to a Python or JavaScript walk, with JS covering defines, calls, imports, and contains. build_graph ingests both and the analyses run on the merged graph. The tree_sitter_javascript grammar is added to the image and CI, and JS extraction is golden-tested against chiasmus's own extractGraph: defines, calls, and imports match exactly, including arrow-const functions and new-expressions not being counted as calls. Tests: 86 under tests/v3-service, all green; ruff clean; Python 3.9 safe. The design doc records the full phase status. The standing caveat is unchanged: the graph logic is unit-tested and golden-pinned against chiasmus, but the effect inside the live run, repair, and symbol-index paths needs the GPU, llama, and sandbox stack to validate.
A wiring audit found two real end-to-end gaps and a few correctness and performance issues. All fixed. The flag never reached the v3-service container, so the whole feature was dead in the Docker deployment, the same bug pattern as ATLAS_RPG_PLANNING. It is now forwarded in docker-compose.yml and documented in .env.example. Phase 3 was half-wired. v3-service produced the symbol-index "graph" field but the proxy discarded it: the response struct had no Graph field and the consumer only used matched and skipped. The proxy now parses symbolGraphNode, and formatGraphNeighborhood folds each matched symbol's callers and callees into the injected system-note context in agent.go, so the neighborhood actually reaches the model. Added Go tests for parsing and formatting. On correctness and performance: the symbol-index handler rebuilt the whole project graph once per matched symbol, so it now builds the graph once and neighborhoods each symbol against it. The in-process Datalog engine raised a KeyError on a caller-supplied rule with an unbound head variable; run now skips unsafe derivations instead of crashing. repair_context gated on Python only while build_graph also handles JavaScript, so it now gates on is_supported. And the transitive-impact dedup hoists its set out of the loop. The datalog max_iter cap was reviewed and left as is: naive fixpoint passes are bounded by the call graph's diameter, not its tuple count, so the cap is not reachable on realistic input and remains a genuine runaway guard. The call_graph endpoint and its closure and complexity analyses stay as deliberate query capabilities with no in-pipeline caller. Tests: 88 under tests/v3-service plus the proxy Go suite, all green; ruff clean.
|
Spent a good while integrating and stress-testing this against Gemma-4-12B — the call-graph engine itself is solid (golden tests pass, the analyses match chiasmus), and I have findings + some additions staged on a branch ( Key finding — the value-delivery point. The engine works, but the two integration sites it shipped with rarely reach the model in a real bug-fix loop:
So the model never actually saw the graph at the moment it was deciding what to edit. What I added on top (keeping your engine as the substrate):
Result: localization went from 0/3 (model edited the wrong/invented function) to reliably editing the right function and removing the original bug. The remaining cap is model content-fidelity (Gemma re-typing a variable with wrong case) — a model-quality issue, not a gap in your engine. Net: this is a great foundation and I want to land it. I'll fold these additions in so the feature delivers where it's actually used. Thanks for the thorough work here — the chiasmus port is clean. |
|
Fantastic, I kind of figured more stuff would get shaken out once you actually exercised it a bit. Good to see that the approach works in principle and just needs a bit more plumbing to be genuinely useful. |
|
Thanks @yogthos — this is merged to What landed in scope of the call-graph work:
It rode along with a broader agent-reliability + per-model-lens pass (see CHANGELOG "Unreleased"), but the call-graph foundation is yours — the clean phases 0–6 structure slotted in well. Thanks for the contribution! |
…xt starvation Builds on yogthos itigges22#125 (call-graph engine). Two themes: deliver structure where the model decides, and stop starving it of the file it edits. Localization (issue #39 / option 3): - Call-graph neighborhood now rides on outline_file AND whole-file read_file (calls / called_by per symbol), scoped to the one file -- no repo-wide scan that missed the target. This is the decision point the model actually uses. - Traceback to directed edit: parse the deepest in-project frame from a run_command crash, quote the offending line, and steer "fix the named function here". If the model re-runs unchanged code, ban the run tools from the next decision's grammar (tracebackExclusion) so it MUST edit. Verified: converts an undirected "it crashes, fix it" into the right-function edit Gemma can do (was 0/3 editing the wrong function; now removes the original bug 3/3). - ast_edit selector miss now lists the file's real symbols instead of a bare "verify it exists" -- grounds hallucinated-symbol retries. Context starvation (root cause of blind edits / hallucinated old_str): - Pin the active file's content in the trim so a long loop can't drop it. - Redundant-read short-circuit only returns the "already in context" pointer when the content is actually still in context; re-serves the full file when it was trimmed. - Size the window to the slot (per-slot minus max_tokens minus margin) instead of a flat 14k cap that left ~10k unused and dropped the file under edit. Other: - Per-turn max_tokens 32768 to 8192 (ATLAS_MAX_TOKENS): bounds content runaways that ran ~18 min to the slot ceiling. - Fix malformed GBNF json-number exponent class (illegal escape) that crashed grammar parsing whenever a number-bearing tool stayed in the enum. Known limit: localization is solved, but clean end-to-end on crash bugs is capped by Gemma re-emitting the same content typo (e.g. Items vs items) in the new body -- a model content-fidelity issue, not a missing rail.
Adds a precomputed, cached call graph and rewires the structural veto to use it, behind ATLAS_CALL_GRAPH (default off, strictly additive). This is the deeper solver-backed layer that issue #39 asked for but that V3.1 never landed: there was no stored call graph, no transitive reachability, and the veto was a shape check rather than real resolution.
The graph engine in v3-service/graph/ is a faithful Python port of the chiasmus call-graph engine. It extracts defines, calls, imports, exports, and contains facts from tree-sitter, resolves Python imports across files, and answers callers, callees, reachability, path, impact, cycles, dead-code, and entry-points natively in linear time.
I omitted the solver since everyday queries are native graph traversal, and Prolog facts are emitted (facts.py) only so an optional solver layer can be added later without re-plumbing if genuinely useful. A per-file content-hash cache makes recompute incremental.
Golden tests compare against chiasmus's own extractGraph and native-analyses of a fixture with classes, methods, decorators, comprehension calls, and aliased and relative imports, and the analyses match including path tie-breaks and impact ordering.