Skip to content

feat(#39): structural call-graph reasoning (ATLAS_CALL_GRAPH)#125

Merged
itigges22 merged 3 commits into
itigges22:mainfrom
yogthos:feat/call-graph-reasoning
Jun 16, 2026
Merged

feat(#39): structural call-graph reasoning (ATLAS_CALL_GRAPH)#125
itigges22 merged 3 commits into
itigges22:mainfrom
yogthos:feat/call-graph-reasoning

Conversation

@yogthos

@yogthos yogthos commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

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.

Yogthos added 3 commits June 10, 2026 15:55
…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.
@yogthos yogthos marked this pull request as ready for review June 10, 2026 20:57
@itigges22

itigges22 commented Jun 14, 2026

Copy link
Copy Markdown
Owner

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 (pr125-integrate).

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:

  • The proxy symbol-index injection only fires when the user message names a symbol, and its project scan (50 files / 500 KB) exhausted its budget before reaching the target file in a non-trivial repo (matched=0 even when I named the symbol).
  • The V3 structural veto only runs after a successful edit triggers V3 — but a weak model fails before that (it never lands the edit).

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):

  1. Call-graph neighborhood on outline_file + whole-file read_file (gated by ATLAS_CALL_GRAPH) — each symbol carries calls / called_by, scoped to the one file, reusing your symbol_neighborhood. This puts the structure on the artifact the model inspects right before it edits.
  2. Traceback → directed edit — parse the deepest in-project frame from a run_command crash, quote the line, and steer "fix function:X here." If the model just re-runs unchanged code, ban the run tools from the next decision's grammar so it must edit. This is the [V3.1] Structural code reasoning: call graph + reachability queries via tree-sitter + solver #39 "stack frame X = function Y" idea made directive.
  3. ast_edit selector-miss now lists the file's real symbols (grounds hallucinated-symbol retries).
  4. Fixed a latent GBNF bug (illegal \- escape in the json-number rule) that crashed grammar parsing when a number-bearing tool stayed in the enum — surfaced by 2.

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.

@yogthos

yogthos commented Jun 14, 2026

Copy link
Copy Markdown
Contributor Author

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.

@itigges22 itigges22 merged commit 0972a34 into itigges22:main Jun 16, 2026
@itigges22

Copy link
Copy Markdown
Owner

Thanks @yogthos — this is merged to main (your three feat(#39) / fix(#39) commits landed with authorship intact via the integration branch). 🙏

What landed in scope of the call-graph work:

  • Call-graph neighborhood at the decision point. The intra-file calls: / called by: edges now ride on outline_file and whole-file read_file of a .py (not a separate step), gated by ATLAS_CALL_GRAPH. Scoped to the single file the model is inspecting, so there's no repo-wide scan that misses the target — structure shows up exactly where the model chooses what to edit.
  • OutlineSymbol gained Calls / CalledBy; the outline handler attaches the neighborhood; ATLAS_CALL_GRAPH is plumbed through docker-compose to the proxy + sandbox.
  • Built on the same [V3.1] Structural code reasoning: call graph + reachability queries via tree-sitter + solver #39 localization thesis: a traceback → directed-edit path (parse the deepest in-project frame, quote the buggy line, steer a minimal edit, ban re-running until it edits) and ast_edit symbol-grounding (a 0-match selector now lists the file's real symbols).

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!

pull Bot pushed a commit to tqa24/ATLAS that referenced this pull request Jun 16, 2026
…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.
itigges22 pushed a commit that referenced this pull request Jul 5, 2026
The call-graph reasoning commits (PR #125) were authored with an email
that isn't linked to a GitHub account, so they don't attribute to
@yogthos on GitHub. .mailmap fixes git-tooling attribution; this commit
itself carries the canonical identity.
itigges22 pushed a commit that referenced this pull request Jul 5, 2026
The call-graph reasoning commits (PR #125) were authored with an email
that isn't linked to a GitHub account, so they don't attribute to
@yogthos on GitHub. .mailmap fixes git-tooling attribution; this commit
itself carries his GitHub noreply identity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants