---
name: nustack-graph
requires-tool: nustack
description: Query the code graph before reading source. An LLM-first codebase index (symbols, call and data references, git history, co-change coupling, ownership) answering whole questions in one call, through `nustack graph`. Use it to localize, scope, and assess blast radius on this repository; use grep/read for string literals, mechanism, and data flow — the skill states which is which. Applies to any task that starts with "where is", "who calls", "what changed", "who owns", "what breaks", or "is this tested".
---

# nustack graph — the index-first method

**If the tool is absent** (`nustack --version` fails): do not stall. Read `.claude/state/decisions.tsv` for `tool.nustack`.

- **`declined`** — fall back to the cold method (grep, targeted reads, `git log`) **silently**. Do not mention the tool, do not re-offer. The user has answered.
- **undecided** (no file, no key, or anything unrecognised) — fall back to the cold method *and* make the offer **once**: this repository would answer "where is / who calls / what changed / who owns" from an index instead of a grep sweep. `npx @nurix/nustack graph overview` runs it without installing anything; `npm install -g @nurix/nustack` puts it on `PATH`. **Ask, never act** — a global npm install modifies the machine, and indexing writes to `~/.codegraph`. Record the answer with `recordDecision` either way, so the question is asked at most once per repo per machine.
- **`accepted` but the binary is gone** — treat as undecided; a failed or removed install is not a standing yes.

**A failed install is `absent`, not an error to retry.** Global installs fail on machines without permission; say so once, fall back, and move on. `npx @nurix/nustack` needs no install at all and is the shorter ladder when one command is all you want.

**An explicit invocation overrides a stored decline.** If the user asks for this skill by name, honour it and re-offer — "stop asking" must never become "never usable".

**A repository that has never been indexed** answers with the tool saying so rather than with a wrong answer. That is the tool degrading correctly, not a failure: `nustack graph index` builds it, under the same ask-first rule.

**Freshness is not your problem.** `nustack graph` re-indexes a stale store before it answers, so a query after a commit reports the new HEAD without anyone running an index first. Pass `--no-refresh` when you deliberately want the index as it stands — answering from a known-old index is a choice you can make, never one you make by accident. This is the one thing that changed when the code graph moved behind `nustack`: there is no staleness ritual left to perform.

Everything below assumes the tool is present.

`nustack graph` defaults to the git root of the working directory — inside this repository, no flags are needed; `--repo <pathOrId>` asks about another one. Every answer states its own confidence per section (`CERTAIN` / `INFERRED` / `UNRESOLVED`), announces anything the token budget dropped, and says what an empty section means. Trust those labels.

## The method order

Localize through the index **before** opening any file, in this order; then read only the files it points at.

1. **`nustack graph find <fragment>`** — symbols and file paths by name fragment. The entry point when you only have issue vocabulary. Symbol names only — it will never match string literals, dict keys, or SQL fragments (grep for those, without shame).
2. **`nustack graph where-defined <symbol>`** / **`nustack graph outline <file>`** — pin the definition; map a big file. `outline` turns a 100+ declaration file into a navigable, line-numbered list — use it instead of reading god files blind. On a large file add `--budget 0` (see traps).
3. **`nustack graph who-calls <symbol>`** / **`nustack graph impact <target>`** — callers, and blast radius (static reach + historical co-change reach, reported separately on purpose: they disagree, and the disagreement is information).
4. **`nustack graph churn --days N --path <dir>`** · **`coupling --path <file>`** · **`owners <path>`** · **`tests-covering <file>`** — the history layer: what's hot, what changes together (including *hidden coupling* — co-change with no static reference, the one fact reading code cannot reveal), who knows this area and how recently, which test file historically guards this file.
5. **`nustack graph god-objects --minSymbols 30`** / **`hotspots`** / **`overview`** — repo-scale orientation and risk.

Then read, with `sed -n` ranges at the lines the index gave you. The index does discovery; reading does diagnosis. Add `--json` when a wrapper needs structure — the answer arrives inside the CLI's machine envelope, `{ nustackJsonVersion, command, ok, data }`.

There is also **`nustack graph blame <file> [--line N]`** — the commits behind a file or one specific line, answered by git at query time; **`nustack graph refs <name>`** — the data-reference layer: where a name is used as a dict/object key, attribute/field, import, or type annotation, with read/write direction, and a cross-language flag when the same name spans Python and TypeScript files (this answers "who reads `x_axis_sort`" / "who writes `chart.query_context`" in one call — the questions that used to need grep); and **`nustack graph drift`** — near-duplicate function bodies crossed with co-change history: similar code whose files never change together is an unfinished refactor waiting to disagree. `who-calls` additionally tabulates which keyword arguments each call site passes, so "which call sites omit this option" is one query.

**`nustack graph status`** reports what the index holds for this repository — indexed HEAD, file and symbol counts, whether it is stale — and **`nustack graph index [--commits N]`** rebuilds it explicitly. Reach for `status` when an answer surprises you and you want to know what it was computed from; you should never need `index` to make a query correct.

## The boundary — what the index cannot answer (fall back immediately, don't retry)

- **Free-text string literals** (SQL templates, prose, format strings) — grep. Identifier-shaped keys and fields ARE indexed (`refs <name>`); anything longer or with spaces is not.
- **Framework reflection** (Marshmallow `@post_load`, signal handlers, `appbuilder.add_api(Cls)` registration) — invisible to call edges. Bare decorator *application* (`@has_access_api`) IS indexed as a reference; what the decorator does and when it runs is not.
- **Data flow and mechanism** ("does this value reach that function", "what order do these run in") — always reading work.

Indexed symbol forms include statement declarations, `const X = () => {}`, named function expressions inside wrapper calls (`typedMemo(function DataTable() {})`), TS/JS class fields, and Python class-body fields (`is_dttm = Column(...)`); nested members render as `Parent › member` in `outline`.

## Traps (each cost a prior agent real time)

- **Bare-name collision.** Call references match by name, not resolved binding. When a name has several definitions, `who-calls` lists every candidate and says so in its subject line — read that section before trusting the call sites, and pass `--scope <path-fragment>` (also on `find`, `where-defined`, `impact`) to narrow to a package.
- **`nustack graph functions` lists the graph's own verbs**, not the repository's functions. For repo functions use `find`. An explicit `--limit` lifts the default token budget, so a full `outline` needs no `--budget 0` incantation (an explicit `--budget` still wins).
- **Churn windows.** `churn --days 180` returning nothing does not mean quiet history — widen the window. Directory-scope churn ranks by raw commit count and buries small-but-critical files; for specific files use `owners`/`coupling` per file.
- **`tests-covering` is co-change evidence, not executed coverage** — it names the right test file to open; it cannot say the specific scenario is tested.
- **Coupling on a busy file has a noise tail** — partners at <15% shared commits are usually PR-cohabitation, not real coupling. Verify a partner with a read before adding it to a touch-list.
- **The history layer sees one window, and the refresh keeps resetting it to 5000 commits.** `churn`, `owners`, `coupling`, `blame` and `tests-covering` answer over the ingested commits, and an index — the automatic one included — takes the most recent 5000 unless `index --commits N` says otherwise. On a deeper repository those answers are true of the window, not of the repository, and the next refresh silently narrows a wider window back. Say "over the last N commits" when you report them, and re-ask for the depth you need.
- **`owners` is authorship weight, not a file-touch tally** — an author is counted once per commit touching the path, however many of the path's files that commit changed. Do not transcribe its numbers as "files changed".
- **The first query after a long gap pays for the re-index.** Absorption is not free: on a large repository the refresh takes tens of seconds before the answer appears. That is one wait, not a per-query cost — and `--no-refresh` skips it when you would rather have the old answer now.

## Division of truth in this repository

`docs/` is the source of truth for **intent** — what a feature is for, its invariants, decisions, vocabulary. The code graph is the source of truth for **facts** — where things are, who calls what, what changed together, who owns what. Never copy live facts (caller counts, churn numbers) into docs as if permanent; never ask the index for intent.
