code-intelligence
For a structure question — who calls X, where is Y used, what imports Z, what breaks if I change this symbol — ask an index that already exists before rebuilding the relationship by hand: it is already built, its answer is structured, and it costs a fraction of the tokens. It is not more precise than a blind
grep— measured on this repository's own source, zero classes met the win bar (§ Measured twice) — so grep is the arm you fall back to routinely and with a stated reason, not grudgingly. This skill routes such questions to the native code-graph engine (ADR-124, Class A) or a consumer-shipped index first. It is the executable side ofexternal-code-graph-interop: orchestrator first, owner where it wins.
When to use
- The question is about relationships between code symbols, not content: callers/callees, references, imports, inheritance, change-impact.
- You are about to
grep/Readacross many files to reconstruct a call or import graph by hand.
When NOT to use: content/semantic questions ("what does this function
mean", "find the string 'password'"), single-file edits, or a repo with no
code-graph and no appetite to build one — plain grep/read is right there.
Procedure
- Detect the source.
agent-config code-graph detect— is a fresh consumergraph.jsonor native cache present? (A consumer-shipped fresh index wins; the native engine covers stale-or-absent — ADR-124 § 2.) - Build if absent and worthwhile. No graph + a repo in the launch set
(PHP / TS / JS)?
agent-config code-graph build(deterministic, LLM-free, ~seconds). Skip for a one-off question in an unsupported stack — grep instead. - Query. Pick the verb:
agent-config code-graph query <symbol>— direct relations of a symbol.agent-config code-graph affected <symbol>— reverse: who calls / references it (the "impact of changing X" question).--since <ref>seeds from a git diff.agent-config code-graph path <a> <b>— how two symbols connect.agent-config code-graph explain <symbol>— 2-hop neighbourhood. Pass--budget <tokens>to cap output.
- Read the confidence.
EXTRACTED= syntactic fact — a symbol declared in the file, or a name bound to the module specifier the file names;INFERRED= resolved by hierarchy, or by a repo-wide same-name lookup with no binding in the file to justify it;AMBIGUOUS= dynamic dispatch / facade, carries candidates — treat its target as one of the candidates, never as certain. Two target shapes are not nodes and resolve to nothing you can read:symbol:<name>(a name this repository does not declare) andexternal:<module>/external:<module>#<name>(a name imported from outside the indexed root —external:node:path). - Fall back honestly. If the graph has no entry for the symbol, grep — and say so: "the graph has no entry for X, so I grepped." If this command is not available, grep and say so.
Output
Every answer built with this skill MUST:
- Name the source that answered —
native,consumer, orgrep-fallback— so the reader knows the provenance and freshness of the relationship claim. - Preserve the confidence class for each relationship surfaced — never
present an
AMBIGUOUSedge as a definite call; list its candidates or say "ambiguous (dynamic dispatch)".
Gotcha
- A stale graph lies confidently. If
detectreports the index is N commits behind, rebuild (agent-config code-graph build --update) before trusting relationship answers, or say the answer is from a stale index. - Dynamic dispatch is honestly ambiguous — where there is anything to be
ambiguous ABOUT. A
$obj->m()whose name matches a method declared somewhere in the repo isAMBIGUOUSand carries those candidates: the engine being honest, not broken — do not "resolve" them yourself by guessing. A dynamic call whose name matches no in-repo method (xs.push(),map.get()) is not emitted at all, and the count is published assuppressed_edge_counts.dynamic_no_candidate. It used to be 39 % of this engine's own graph, pointing atsymbol:push, which is not a node and which no query verb could reach. - The AMBIGUOUS share is a property of the code, not a constant. Measured
2026-09-04 over the three roots the v2 benchmark uses:
AMBIGUOUSis 0 of 495 edges under the package's own graph-engine root, 0 of 181 undersrc/shared, and 121 of 4,002 (3.0 %) under its council root — class-free TypeScript has almost no in-repo method to be ambiguous between. A Laravel codebase, where facades and injected services dispatch onto real in-repo methods, is the opposite case. Read the counts in the graph rather than a remembered ratio.
Measured twice, and it has not won (v2, 2026-08-29)
Pre-registered, published whichever way it landed:
internal/bench/reports/code-graph-vs-grep-inrepo-v2-2026-08-29.md. The native
engine scored against disciplined git grep over three in-repo TypeScript
roots, 19 questions, per-class bars fixed before the run and unchanged from v1.
| Class | grep R | graph R | Δ pp | grep P | graph P | verdict |
|---|---|---|---|---|---|---|
callers |
1.000 | 1.000 | +0.0 | 0.611 | 0.667 | TIE |
transitive-impact |
0.611 | 0.500 | −11.1 | 1.000 | 0.667 | NULL |
path-between |
0.917 | 1.000 | +8.3 | 0.778 | 1.000 | TIE |
references |
1.000 | 0.333 | −66.7 | 0.833 | 0.333 | NULL |
Zero classes met the win bar. On path-between the graph is exact and is the
only class where it out-precises grep; it still ties, because the delta is +8.3 pp
against a +10 pp bar that was fixed before the run.
The v1 run of 2026-08-28 published a false root cause, corrected 2026-08-29.
It reported path-between as VOID because "both arms measured nothing". Only
the grep arm did. The graph answered all three questions and v1's scorer discarded
the answer — it compared each returned symbol against the whole probe string
"cmdBuild -> getParser". v1 also never invoked the shipped path <a> <b> verb,
and counted unresolved symbol: pseudo-nodes as files, which is the sole reason
its callers verdict was NULL with recall tied. v1's numbers are not retro-edited
— they were faithful to v1's own registration — and v1's report now carries the
correction. Do not quote a path-between delta near +89 pp: that figure comes
from repairing the graph arm and leaving grep on the broken probe.
The rule's own wording was corrected by this result. external-code-graph-interop
used to open by saying a committed index answers "far more precisely than a fresh
grep". That was never measured, and when it was, it was false for the native
engine on this repository's own code. The rule now gives the reasons that survive
measurement — an index that exists is already built and structured, so it is the
cheap first question — and drops the precision claim its own benchmark refuted.
Re-run after the extractor repair (2026-09-04) — still no class won
internal/bench/reports/code-graph-vs-grep-inrepo-v2-rerun-2026-09-04.md. The
SAME registration, the same corpus SHA, the same per-class bars and the same
arm-B verbs, re-run after the import-binding repair. The 2026-08-29 report is
untouched; this is a second report beside it, which is how v1 was handled too.
| Class | graph R then | graph R now | graph P then | graph P now | verdict then → now |
|---|---|---|---|---|---|
callers |
1.000 | 1.000 | 0.667 | 0.667 | TIE → TIE |
transitive-impact |
0.500 | 0.611 | 0.667 | 1.000 | NULL → TIE |
path-between |
1.000 | 1.000 | 1.000 | 1.000 | TIE → TIE |
references |
0.333 | 1.000 | 0.333 | 1.000 | NULL → TIE |
Zero classes met the win bar, again. Two NULLs became TIEs and nothing regressed, so the routing verdict below is unchanged — a TIE is not a win, and no bar was renegotiated after the repair.
Read with two caveats the report states in full. A measured root is live source:
src/shared is byte-identical between the runs, the council root moved
by 27 files on main in between, and the graph-engine root IS the engine, so
its content necessarily moves whenever the engine does — which is also why the
GREP arm's macro precision moved (0.806 → 0.764) in a run that changed nothing
about grep. Both classes that changed verdict did so on rows whose root did not
drift: references moved on the graph-engine and shared roots, transitive-impact on
shared alone.
No class is graph-first. Query the index first because an index that already exists is cheap to ask and its answer is structured — not because it answers better. When it returns nothing, that is the common case, and grep remains the arm to fall back to rather than a grudging afterthought.
The sentence that stood here — "the arm that won every valid class on this corpus" — is withdrawn. It is the framing v1's own report forbids: that report says the defensible statement is "zero classes met the pre-registered win criterion", not that grep proved superior. It was also false on its own terms under v2: grep wins two classes, ties two, and is out-precised on both ties.
Scope, stated so this table is not over-read: it measured the native engine
on this repository's TypeScript, with a corpus of 19 questions. It is not
comparable to the 2026-07-28 external-corpus run, it is not comparable to v1
either (corpus, arm-B verb set and scorer all moved), and it says nothing about a
consumer-shipped SCIP index. Literal-string probes are reported as a separate
capability-boundary class with no floor derived from them: a symbol index
cannot answer them at all, which is where grep stays necessary rather than a
defect in the engine.
Staleness delivery — which hosts get it for free, and which do not
Migrated verbatim from external-code-graph-interop so the obligation stays in
the rule and the per-host delivery detail is paid only on activation.
On a host with a verified pre_tool_use contract, the code-graph-context
concern supplies this for free: once per session, on the first search or
code read, it delivers the graph's state as structured additionalContext —
code-graph: fresh or code-graph: N commit(s) behind. It is silent when no
graph exists, so hearing nothing is not evidence that the graph is fresh.
On every other host that carrier does not exist, and this section is the
delivery: run agent-config code-graph detect --format json before trusting a
relationship answer, and say which state you got. Which hosts those are is
agent-config hooks:status — resolved from the platform table, never guessed
from the host's name.
Either way the disposition is the same: behind:N → agent-config code-graph refresh first, or use grep and say so. This is a freshness obligation, not an
ordering claim; the rule's query-first ordering still governs which source to
reach for.
The four gate verbs — a diff, not a symbol (2026-09-08)
query / affected / path / explain answer a question about a SYMBOL.
Four more answer a question about a DIFF, and they are documented here rather
than in § Procedure for a mechanical reason worth stating: step 4.3 of
road-to-a-graph-that-is-shipped verifies that this file's line 164 — the
No class is graph-first sentence — is unchanged, and inserting anything
above it moves that line. So the ordering claim K5 protects stays byte-fixed
and the new material sits below it.
Each of the four walks only edges whose resolution mechanism is a stated fact (never a repo-wide same-name guess, never an untyped receiver), and each prints the mechanisms it accepted, the mechanisms it refused, and the graph's staleness — so a narrow answer is distinguishable from a decoupled one:
agent-config code-graph impact --diff <rev>— callers, dependents and test files reachable from the changed symbols, plus the producing edges and a minimal read set.agent-config code-graph tests-for <symbol>— the test files that import it. An empty list is a finding, not an error.agent-config code-graph untested --diff <rev>— changed symbols that no test file imports.agent-config code-graph dead [--entry-points F | --accept-missing-exports]— symbols with no accepted reference that no declared entry point names. It refuses rather than answering while an entry-point source cannot be read: this engine records no exportedness, so without a supplied list an exported-but-unimported symbol is indistinguishable from a dead one, and a confident false "dead" invites a deletion the graph cannot justify. Nothing it lists is a deletion recommendation.
These answers reach an agent over MCP without shelling out, and the mapping is not one tool per verb — corrected after an independent review found this paragraph claiming "the same five answers" directly after a list of four:
| MCP tool | answers |
|---|---|
graph_impact |
impact --diff, and untested --diff via untested_only: true |
graph_tests_for |
tests-for |
graph_dead |
dead |
graph_query |
query |
graph_path |
path |
untested has no tool of its own; it is a flag on graph_impact, because the
two answer the same question over the same diff and a sixth tool would have cost
standing context for a boolean. graph_impact is the only one that is not
read-only — it runs git diff --name-only to resolve its rev — and
graph_dead's refusal arrives as status: "refused", never as status: "ok"
with an empty list.
No ordering claim is added here, and no measurement claim either. These
verbs are cheaper than reconstructing the same relationship by hand — that is a
claim about the ALTERNATIVE being a manual reconstruction, not about grep.
Nothing on this page says they beat grep, none of the four appears in the v2
benchmark corpus (which measures callers, transitive-impact, path-between
and references), and § Measured twice still governs that question.
Do NOT
- Do NOT rebuild a fresh consumer-shipped index — query it (interop courtesy).
- Do NOT block or defer a read waiting on the graph — it is an accelerator, not a gate.
- Do NOT treat the cache as a source of truth for anything beyond the last build; it is a rebuildable artifact.
See also
external-code-graph-interop— the rule that routes structure questions here.source-discovery— evidence-before-structure discipline; the graph is one evidence source, still confirmed against the real code when load-bearing.agent-config code-graph— the CLI surface over the engine (build / query / detect / affected).discovery_graph— this suite's OWN artifact relation-graph (affected/explain). The external code-graph is the source-code analogue; keep the two apart when answering.