## Code Graph (Phase 1 Step 2.6 + Phase 5 Step 3)

<!-- toc -->
- [Phase 1 Step 2.6  -  query before dispatching Explore](#phase-1-step-26---query-before-dispatching-explore)
- [Phase 5 Step 3  -  refresh after the branch changed code](#phase-5-step-3---refresh-after-the-branch-changed-code)
- [What the report answers that a file-level view cannot](#what-the-report-answers-that-a-file-level-view-cannot)
- [The graph is drawable, and one place already asks for it](#the-graph-is-drawable-and-one-place-already-asks-for-it)
<!-- /toc -->

A deterministic, LLM-free map of what a repo declares and what refers to what,
written to `~/.claude/knowledge/<project>/code-graph.json`. Gated by
`prefs.global.codeGraph.enabled` (default `false`); with it off, Phase 1 and
Phase 5 behave exactly as they did before. Design, trade and measurements:
`docs/adr/0010-own-code-graph.md`.

### Phase 1 Step 2.6  -  query before dispatching Explore

Runs only when a rule file exists for `detectedStack`. A stack with no rule file
is reported as unsupported, never guessed at.

```bash
GRAPH_PATH="$HOME/.claude/knowledge/$(basename "$PROJECT_ROOT")/code-graph.json"

node $HOME/.claude/scripts/graph-report.mjs --graph "$GRAPH_PATH" --status
node $HOME/.claude/scripts/graph-build.mjs --root "$PROJECT_ROOT" --stack "$STACK" \
  --max-nodes "${prefs_codeGraph_maxNodes:-200000}" --json
node $HOME/.claude/scripts/validate-code-graph.mjs "$GRAPH_PATH"
node $HOME/.claude/scripts/graph-query.mjs "$TASK_TITLE $TASK_DESCRIPTION" \
  --graph "$GRAPH_PATH" --budget "${prefs_codeGraph_queryBudget:-2000}" --json
node $HOME/.claude/scripts/graph-affected.mjs "<symbol the task names>" \
  --graph "$GRAPH_PATH" --depth 2 --json
```

`GRAPH_PATH` is derived once and passed to every call. The query, affected and
report scripts default it from the CWD's basename, and a run happens in a
worktree whose basename need not equal the repo's, so a defaulted path can point
at a graph that was never built. Pass it.

Build when there is no graph at all, and when `baseCommit` no longer matches
HEAD. The missing case is the one that matters in practice: `architecture.md`
and its siblings are written in Phase 5, which is the phase a run is least
likely to reach, so a repo can have a long history of tasks and an empty
knowledge directory. The graph must not inherit that. `--status` exits 1 on a
missing file, and that exit means build, not skip.

A build is not usable until
`validate-code-graph.mjs` exits 0: a graph whose edges point at missing nodes
truncates traversals silently, so a non-zero exit means skip the injection and
run Explore as if no graph existed.

`graph-query` output becomes the Explore agents' starting file set;
`graph-affected` output feeds `analysis.touchedAreas[]`.

Use it to narrow an open-ended search, not to replace a grep for a name the task
already spells out. Measured on a 4,300-file Swift app at a fixed 30k retrieval
budget, it roughly doubled coverage at under half the cost on domain-word
questions and lost narrowly to `grep -lw` on exact type names.

### Phase 5 Step 3  -  refresh after the branch changed code

Runs when `prefs.global.codeGraph.enabled` and `prefs.global.codeGraph.autoRefresh`
are both true.

```bash
GRAPH_PATH="$HOME/.claude/knowledge/$(basename "$PROJECT_ROOT")/code-graph.json"

node $HOME/.claude/scripts/graph-build.mjs --root "$PROJECT_ROOT" --stack "$STACK" \
  --max-nodes "${prefs_codeGraph_maxNodes:-200000}" --json
node $HOME/.claude/scripts/validate-code-graph.mjs "$GRAPH_PATH"
node $HOME/.claude/scripts/graph-report.mjs --graph "$GRAPH_PATH"
```

The rebuild costs no API tokens, so it runs every task rather than on a staleness
heuristic. A non-zero validator exit keeps the previous graph and logs
`knowledge.graph_invalid`; it never fails the run  -  a stale graph is a degraded
Phase 1, not a broken deliverable.

### What the report answers that a file-level view cannot

`GRAPH_REPORT.md` ends with **Symbols nothing else references**: symbols no other
file in the repo names. The older "unconnected files" section only ever found
files with NO edge at all, so a file imported for one symbol while three of its
other exports were dead looked healthy.

It is candidates, never verdicts, and it gates nothing. The extractor is regex
over comment-stripped source, not a parser (ADR-0010), so dynamic dispatch,
reflection, string-keyed lookup and a public API consumed outside this repo are
indistinguishable from dead code here. Four classes are therefore excluded and
COUNTED rather than listed, because they could not carry a reference edge however
heavily they are used: a kind outside the stack's `referenceKinds`, a name
declared in more than one place (the builder drops ambiguous tokens), a nested
declaration, and anything declared in a test file. Symbols referenced ONLY from
tests are listed separately - that is not dead code, it is code whose only
consumer is its own test, which is worth knowing before a plan calls it
load-bearing.

### The graph is drawable, and one place already asks for it

The PR body's Impact Analysis, part 3, asks which symbols and files a change
reaches. That is `graph-affected.mjs`'s question, and until now the answer was
re-typed as prose by a model while the measurement sat on disk unread.

```bash
node $HOME/.claude/scripts/graph-mermaid.mjs "<symbol[,symbol]>" [--depth N] [--max-nodes N]
```

It emits a fenced `flowchart` and nothing else - no renderer, no plugin, no
dependency, because mermaid is text and GitHub renders it natively in pull
requests, issues and markdown files. Traversal is not reimplemented: `findByName`
and `affected` are imported from `graph-affected.mjs`, so the diagram and the
text report cannot disagree about what is affected.

Three properties that are enforced rather than promised
(`smoke-graph-mermaid.sh`):

- Every drawn node and edge resolves back into `code-graph.json`, with the edge
  kind it claims. A diagram is read as fact and checked less than prose, so an
  invented edge is the expensive failure.
- Over `--max-nodes` the leftover count is printed inside the diagram, not
  dropped. A small picture of a large blast radius reads as reassurance.
- The graph's `baseCommit` is printed beside it. A graph built before the change
  draws an older tree, and nothing else in the PR would reveal that.

Exit 1 with a reason on stderr means no graph or no such symbol. The caller
records the gap and writes the prose alone; it never hand-draws a replacement.

Jira is not a target: its renderer turns the fence into a literal
`{code:mermaid}` block. Confluence renders it through the `ac:name="mermaid"`
macro when the space carries the plugin (`channels/confluence.md`).
