Real input, real output

What you actually do, in order.

Every block on this page is a real call and its real answer, captured by running nao mcp against nao's own source — the same JSON-RPC an agent speaks. Nothing is illustrative. If a tool's output changes, this page changes with it.

captured from nao@7baad23 — “add a new theme”

First decision

Graph, or tools?

They answer different kinds of question, and picking wrong is why people bounce off code-graph tools.

The graph is for questions you can't phrase yet. You don't know the vocabulary, so you can't name the function to ask about. Shape, clusters, what sits at the centre — the eye is better at that than any ranked list.

The tools are for questions you can phrase. The moment you can say a name — “what breaks if I change detect_smells” — a list beats a picture, and it fits in an agent's context window.

One hard rule: never open a large repository at entity level. nao's own source is 4,823 entities and 22,417 relationships. Drawn all at once that is a grey disc, and that's not a rendering flaw — it is what twenty thousand relationships look like.

SituationStart withNot with
Dropped into a repooverview, then map one folderthe root map — 206 files is a wall
A ticket naming a featureoverview focuscontextreading the folder top to bottom
“Where is this risky?”hotspots, then qualityopening the biggest file
About to change somethingimpacttests_forgrep for the name
You just changed something--check--drifta wiki page nobody re-reads
No idea what you're looking atthe graph, at landmark levelthe graph, at entity level

Workflow one · about twenty minutes

Understanding a project you've just inherited.

The goal is not to understand the code. It is to know the shape well enough that the next ticket lands somewhere specific instead of nowhere.

  1. 01

    Read the domain, not the code

    If the project has an Elevator spec, this is the whole product surface in one screen — categories, the features under them, and the file each one claims. Forty-four lines to replace a week of orientation.

    call overview {} 44 lines
    # Elevator spec format
    # - Indentation = containment (parent → child).
    # - Kinds: e=Extension (optional top-level bundle containing Categories + Concepts; only used when the project has separable extensions/plugins), c=Category (grouping of Features), f=Feature (capability), fu=Functionality (qualified <feature>.<verb>; verb on a Feature), @=Concept (cross-cutting concern referenced by multiple Features), ui=UI Page.
    # - Bracketed edges: [where: ui.X] = entity appears at UI page X; [refs: f.X] = entity cross-references Feature X; [cr: PATH] = code reference (file/folder implementing this entity); [cr.<tag>: PATH] = code reference partitioned by layer (common tags: fe=frontend, be=backend).
    # - [UNRESOLVED] = the named entity has no definition in any parsed file; the reference is a broken link.
    # - `← target` (focus mode only) marks the entity the artifact is centred on.
    # - Trust: structure (hierarchy, references) is analyzer-checked, but descriptions and cr: paths are authored, not derived from code — they can lag recent changes. Treat them as strong leads, not ground truth; verify mechanism details against the cr: target before relying on them.
    # Project map (Elevator)
    > 5 categories, 13 features, 12 functionalities, 1 concepts, 0 ui pages
    
    c agent_tools — The agent-facing surface over the graph: read-only MCP tools pulled on demand, … [cr: src/mcp/]
      f dead_code — Removal candidates as an MCP tool call: fan-in 0, minus the populations for whi… [cr: src/mcp/tools.rs]
      f overview — Domain-level Elevator map (or focus bundle) as an MCP tool call — the onboardin… [cr: src/mcp/tools.rs]
      f push_review — Push mode: nao's structural signal arrives without being asked. Fresh-process c… [cr: src/mcp/push.rs]
        fu pr_report — The CI leg: assess_change rendered as one PR comment, edited in place by marker…
        fu self_review — Quiet-when-clean Stop-hook review: only new smells, cycles, and above-floor com…
    c code_graph — Typed entity/relationship graph built from heterogeneous sources — one parser p… [cr: src/parser/, src/analyzer/, src/graph.rs]
      f metrics — Per-entity complexity and coupling (cyclomatic, cognitive, nesting, fan-in/out,… [cr: src/graph.rs]
      f parse_store — Content-hashed per-file parse cache on disk, and the whole of the incremental m… [cr: src/analyzer/parse_store.rs]
        fu invalidate — Entries are scoped by a generation key derived at build time: build.rs hashes s… [cr: src/analyzer/parse_store.rs]
      f parsers — One parser per language behind the LanguageParser trait, deliberately independe… [cr: src/parser/]
        fu infer_types — Reconstructs receiver types so a method call resolves to Type::method rather th… [refs: f.resolution; cr: src/parser/rust/inference.rs]

    The header is the format legend the tool prints for its consumer — a model reading this cold needs to know that cr: is a code pointer and that descriptions are authored, not derived.

    No spec? Then start one folder down, not at the root. A depth-1 map of nao's root lists 206 files — accurate, and useless as a first read. Pick the folder the README or the build file points at and map that.

  2. 02

    Find where it hurts before you find what it does

    Complexity alone tells you which code is ugly. Crossed with git churn it tells you which ugly code is actually costing money — and where the next bug is likely to come from.

    call hotspots { path: "vscode-extension", limit: 6 } 14 lines
    # Hotspots of ./vscode-extension (last 180 days)
    Risk = commits in window × LOC-weighted avg composite score. Renames count as fresh paths.
    
    - [risk 1.00] vscode-extension/src/extension.ts — 16 commits, avg pressure 0.06; worst: function `activate` (L43, loc 0, out 112)
    - [risk 0.24] vscode-extension/src/educatorViewProvider.ts — 8 commits, avg pressure 0.03; worst: method `refresh` (L84, loc 0, in 1, out 8)
    - [risk 0.21] vscode-extension/src/server.ts — 4 commits, avg pressure 0.05; worst: method `start` (L45, loc 0, out 18)
    - [risk 0.15] vscode-extension/src/educatorHoverProvider.ts — 3 commits, avg pressure 0.05; worst: method `provideHover` (L27, loc 0, out 13)
    - [risk 0.08] vscode-extension/src/controlsViewProvider.ts — 3 commits, avg pressure 0.03; worst: method `resolveWebviewView` (L67, loc 0, out 9)
    - [risk 0.06] vscode-extension/src/scopeTreeProvider.ts — 2 commits, avg pressure 0.03; worst: function `fetchIndex` (L340, loc 0, out 10)
    - [risk 0.06] vscode-extension/src/sideViewProvider.ts — 3 commits, avg pressure 0.02; worst: function `fetchScope` (L552, loc 0, out 12)
    - [risk 0.05] vscode-extension/src/educatorProblemsViewProvider.ts — 2 commits, avg pressure 0.03; worst: method `refresh` (L107, loc 0, out 9)
    - [risk 0.04] vscode-extension/src/diffViewProvider.ts — 2 commits, avg pressure 0.02; worst: method `resolveWebviewView` (L33, loc 0, out 9)
    - [risk 0.03] vscode-extension/src/panel.ts — 3 commits, avg pressure 0.01; worst: class `VisualizerPanel` (L162, loc 0, methods 26, in 1)
    … and 2 more files with non-zero risk.

    Risk is commits in the window times LOC-weighted refactor pressure. A gnarly file nobody touches sinks; a moderately complex file edited sixteen times rises. Scoped to one half of the repo here — the tool takes a path, and a narrower ranking is usually the useful one.

  3. 03

    Map the folder you were pointed at

    Files, their entities, complexity and coupling per entity — one call instead of opening six files to reconstruct the same picture in your head.

    call map { path: "src/analyzer", depth: 2 } 59 lines
    # Map of ./src/analyzer
    6 files — 5 constant, 1 enum, 128 function, 10 module, 13 struct
    
    ./src/analyzer/dependency_resolver.rs (2 entities)
      struct DependencyResolver (L11, loc 19, methods 13, out 3)
      struct TransitiveDependencyResolver (L470, loc 4, methods 3, out 2)
    ./src/analyzer/file_walker.rs (3 entities)
      function is_test_path (L18, loc 4, cx 2, cog 1, in 1, out 3)
      struct FileWalker (L24, loc 5, methods 5, out 2)
      module tests (L188, loc 0, methods 1)
    ./src/analyzer/lsp_tracer.rs (17 entities)
      struct CallSite (L35, loc 6, in 3)
      constant DEFAULT_TIMEOUT_SECS (L50, loc 0)
      constant IDLE_SECS (L55, loc 0)
      function enabled_by_env (L62, loc 3, cx 2, cog 2, in 1, out 2)
      function resolve_exact_calls (L70, loc 36, cx 5, cog 5, in 1, out 18)
      function run (L110, loc 112, cx 15, cog 25, in 2, out 51)
      enum Encoding (L227, loc 4, in 2)
      function encode_col (L235, loc 9, cx 3, cog 1, in 1, out 5)
      struct LspClient (L249, loc 7, methods 11, out 4)
  4. 04

    Ask what's wrong with it

    Smells with remediation hints, then everything ranked by refactor pressure. This is the list you bring to a planning meeting.

    call quality { path: "src/analyzer" } 21 lines
    # Quality of ./src/analyzer
    
    ## Smells (1)
    - function `discover_files` — ./src/analyzer/mod.rs:112 (L112, loc 4, cx 1, cog 0, in 1, out 3, ⚠ Feature Envy)
    
    Remediation hints:
    - Feature Envy: Move this method to the type it mostly interacts with, or extract shared logic.
    
    ## Top 10 by refactor pressure
    - [1.40] function `validate_elevator_imports` — ./src/analyzer/mod.rs:335 (L335, loc 111, cx 18, cog 47, in 1, out 42)
    - [1.36] function `run` — ./src/analyzer/lsp_tracer.rs:110 (L110, loc 112, cx 15, cog 25, in 2, out 51)
    - [1.19] function `find_entity_by_name_near` — ./src/analyzer/dependency_resolver.rs:396 (L396, loc 60, cx 18, cog 46, in 4, out 24)
    - [0.98] function `create_branch_entities` — ./src/analyzer/mod.rs:730 (L730, loc 88, cx 9, cog 15, in 1, out 26)
    - [0.96] function `resolve_trait_methods` — ./src/analyzer/dependency_resolver.rs:206 (L206, loc 54, cx 11, cog 26, in 1, out 21)
  5. 05

    Now look at it

    Only now — with names you recognise — does the picture pay. Open at landmarks, find the cluster you just read about, then open the level up. Doing this first, at full detail, is the mistake everyone makes once.

    analysing…

    Same ladder in the real tool: scope to a folder in Visual Scopes, set the aggregation to file, then drop to entity once the scope is small enough to read. Try it on a hosted repo →

Workflow two · about five minutes

Understanding one feature, because a ticket says so.

Worked here on a real one: “change how code smells are detected”. Six calls, no file opened until the last one.

  1. 01

    Ground the ticket in the domain

    Which feature is this, what does it claim to do, which file implements it, and what sits next to it. The focus bundle gives you the ancestors and the siblings — context you'd otherwise get by asking someone.

    call overview { focus: "f.smells" } 19 lines
    # Elevator spec format
    # - Indentation = containment (parent → child).
    # - Kinds: e=Extension (optional top-level bundle containing Categories + Concepts; only used when the project has separable extensions/plugins), c=Category (grouping of Features), f=Feature (capability), fu=Functionality (qualified <feature>.<verb>; verb on a Feature), @=Concept (cross-cutting concern referenced by multiple Features), ui=UI Page.
    # - Bracketed edges: [where: ui.X] = entity appears at UI page X; [refs: f.X] = entity cross-references Feature X; [cr: PATH] = code reference (file/folder implementing this entity); [cr.<tag>: PATH] = code reference partitioned by layer (common tags: fe=frontend, be=backend).
    # - [UNRESOLVED] = the named entity has no definition in any parsed file; the reference is a broken link.
    # - `← target` (focus mode only) marks the entity the artifact is centred on.
    # - Trust: structure (hierarchy, references) is analyzer-checked, but descriptions and cr: paths are authored, not derived from code — they can lag recent changes. Treat them as strong leads, not ground truth; verify mechanism details against the cr: target before relying on them.
    # Focus: f smells
    > ancestors, siblings, target subtree, and cross-cutting concepts
    
    c code_graph — Typed entity/relationship graph built from heterogeneous sources — one parser per language, metrics engine, deterministic output. [cr: src/parser/, src/analyzer/, src/graph.rs]
      f smells — God Class, Dispatcher, Feature Envy, Shotgun Surgery and Data Bag, derived from metric combinations after they settle. Data Bag is informational — a Kotlin data class is the same shape without the same problem. [cr: src/graph.rs]   ← target
    
    # Siblings (other children of c.code_graph)
  2. 02

    Check it doesn't already exist

    Thirty seconds that regularly saves an afternoon — and on a legacy codebase, saves you writing the third copy of a helper.

    call similar { query: "detect smells from metrics" } 5 lines
    # Similar to "detect smells from metrics"
    
    - [0.63] function `detect_smells(&mut self)` — ./src/graph.rs:599 (matched: detect, smells)
    - [0.57] property `metrics` — ui/src/transform.ts:25 (matched: metrics, smells)
    - [0.57] property `metrics` — vscode-extension/src/panel.ts:27 (matched: metrics, smells)
  3. 03

    Pull the edit pack

    The target's full source, plus the signatures of everything it uses and everything that uses it, each with a file and line. This is the call that replaces five file reads — and note what it tells you before you've read a line: cyclomatic 27, cognitive 67, flagged Dispatcher.

    call context { entity: "detect_smells" } 101 lines
    # Context for function `detect_smells` (in struct `DependencyGraph`) — ./src/graph.rs:599 (L599, loc 90, cx 27, cog 67, in 1, out 25, ⚠ Dispatcher)
    
    ## Source
    ```
    fn detect_smells(&mut self) {
            use crate::models::{SmellKind, Thresholds};
    
            let t = Thresholds::default();
    
            // --- Feature Envy pre-pass: group outgoing dependency edges by
            //     target entity's parent, per source entity. ---
            let mut outgoing_by_parent: HashMap<NodeIndex, HashMap<Option<String>, u32>> =
                HashMap::new();
            let mut outgoing_total: HashMap<NodeIndex, u32> = HashMap::new();
            for edge_idx in self.graph.edge_indices() {
                let rel = &self.graph[edge_idx];
                if !rel.kind.is_dependency() {
                    continue;

    Clipped here; the real answer is 101 lines — the whole function plus its neighbourhood.

  4. 04

    Ask who breaks

    Direct dependents first — those break on a contract change — then the blast radius level by level. Run it on the type you're touching as well as the function: a struct's radius is usually the honest answer to “how big is this change”.

    call impact { entity: "detect_smells" } 28 lines
    # Impact of function `detect_smells` (in struct `DependencyGraph`) — ./src/graph.rs:599 (L599, loc 90, cx 27, cog 67, in 1, out 25, ⚠ Dispatcher)
    
    ## Uses (1) — code this entity relies on, plus 31 external/unresolved targets not listed
    - calls ·heuristic function `default` — ./src/models/thresholds.rs:70
    
    ## Used by (1) — direct dependents, first to break on a contract change
    - calls ·heuristic function `from_analysis` — ./src/graph.rs:75
    
    ## Blast radius to depth 2 (19 entities beyond direct dependents)
    - [depth 2] function `write_json_with_cancel` — ./src/server/state.rs:51
    - [depth 2] function `rehydrate_one` — ./src/server/repo.rs:515
    - [depth 2] function `analyze_repo` — ./src/server/repo.rs:259
    call impact { entity: "DependencyGraph" } 136 lines
    # Impact of struct `DependencyGraph` — ./src/graph.rs:12 (L12, loc 13, methods 47, in 59, out 6, ⚠ Shotgun Surgery)
    
    ## Uses (4) — code this entity relies on, plus 2 external/unresolved targets not listed
    - uses type struct `CodeEntity` — ./src/models/entity.rs:9
    - uses type struct `Relationship` — ./src/models/relationship.rs:7
    - uses type struct `FileMetrics` — ./src/models/scope_metrics.rs:125
    - uses type struct `ModuleMetrics` — ./src/models/scope_metrics.rs:133
    
    ## Used by (59) — direct dependents, first to break on a contract change
    - uses type function `compute_diff` — ./src/diff.rs:174
    - uses type function `analyze_at` — ./src/diff.rs:383
    - uses type function `write_diff_outputs` — ./src/diff.rs:399
    - uses type function `filter` — ./src/graph.rs:1369
    - uses type function `graph_of` — ./src/graph.rs:1600

    Same question about the type that owns it. 136 lines of answer — this is the one that stops a “small” refactor at the planning stage.

  5. 05

    Ask how control gets there

    The shortest dependency path, hop by hop, with the relationship kind on each. The flow question you'd otherwise answer by reading upward through call sites.

    call trace { from: "from_analysis", to: "pick_nearest" } 4 lines
    # Trace: function `from_analysis` (./src/graph.rs:75) → function `pick_nearest` (./src/graph.rs:1527)
    
    - function `from_analysis` (./src/graph.rs:75) —calls ·heuristic→ function `pick_nearest` (./src/graph.rs:1527)
    - function `from_analysis` (./src/graph.rs:75) —calls ·heuristic→ function `pick_nearest` (./src/graph.rs:1527)
  6. 06

    Find the net before you jump

    Which tests actually reach this entity, directly or transitively — so you know whether a safety net exists before you write one, and which subset to run instead of the whole suite.

    call tests_for { entity: "pick_nearest" } 28 lines
    # Tests reaching function `pick_nearest` — ./src/graph.rs:1527 (searched 3 dependency hops)
    _20 of 20 reached only via a heuristic call edge — treat those as maybe, not proof._
    
    ./src/analyzer/mod.rs (2 tests)
      function `analysis_is_deterministic_across_runs` — L1041 (2 hops, via heuristic edge)
      function `a_field_type_from_another_file_resolves_the_call` — L1150 (2 hops, via heuristic edge)
    
    ./src/graph.rs (16 tests)
      function `graph_of` — L1600 (2 hops, via heuristic edge)
      function `module_qualified_callee_resolves_to_the_free_function` — L1751 (2 hops, via heuristic edge)
      function `crate_prefixed_module_path_also_resolves` — L1759 (2 hops, via heuristic edge)
      function `ambiguous_module_stem_is_declined_not_guessed` — L1768 (2 hops, via heuristic edge)

    Read the second line: the tool marks that all 20 were reached through heuristic call edges — 'maybe, not proof'. It tells you when to distrust it, which is the only reason to trust it.

Then edit, then check your own work

Editing is ordinary — your editor, your LSP, nao doesn't replace either. The step people skip is the last one: assess_change diffs your working tree against a git ref and reports what the change did to complexity and smells, before anyone else sees it. The full agent loop →

Workflow three · about three minutes

After the change: leave the trail, then prove it holds.

This is the step teams skip, and the reason onboarding costs what it does. It is also where Elevator meets the documents you already keep: the ADR records why, the spec records what and where, and one line joins them.

  1. 01

    Deepen only the branch you touched

    The knowledge is cheap the day you have it and expensive to reconstruct later — but only for the part you actually worked on. Adding depth elsewhere is how a spec becomes a document nobody trusts. One verb, one sentence of mechanism, one code reference.

    spec/code_graph.elv
    f smells {
        d: "God Class, Dispatcher, Feature Envy, Shotgun Surgery, Data Bag —
            derived from metric combinations after they settle."
        cr: "src/graph.rs"
        fu detect
    }
  2. 02

    Point the feature at the decision

    If the change was governed by an ADR — or created one — a tagged code reference makes that link checkable. cr.adr: is a convention, not a parser feature; the tag vocabulary is free-form, and that is exactly why it costs nothing to adopt.

    spec/code_graph.elv
    f parsers {
        d: "One parser per language behind the LanguageParser trait."
        cr: "src/parser/"
        cr.adr: "docs/adr/0001-per-language-parsers-stay-independent.md"
    }
  3. 03

    Let the tools be the radar

    --check separates errors from hints: broken references must be fixed, while “empty” and “orphan” are a to-deepen list — unfinished is a legitimate permanent state. --drift then verifies the anchors: every code reference resolves, and every identifier named in a description still exists in the file that claims it.

    call elevator { argv: "spec/ --check" } 3 lines
    hint: empty: c educator has no Features
    
    0 error(s), 1 hint(s).
    call elevator { argv: "spec/ --drift --code-root ." } 5 lines
    # Elevator drift report
    > code root: .
    > 29 cr reference(s): 21 resolved, 0 missing — 15 identifier(s) checked in 284 file(s), 0 unanchored
    
    ✓ No drift detected: every cr path resolves and every identifier in `d:` is anchored in the claimed code.

    nao's own spec, clean. The hint above is the honest state of one category nobody has deepened yet — it is reported, not failed.

  4. 04

    And when someone deletes the ADR

    The whole point of the join. Retire a decision record without touching the spec and the anchor dangles — reported as an error, exit code 1, the same signal a failing test gives CI. A stale link stops being something a reader has to notice.

    call elevator { argv: "spec/ --drift --code-root ." } 8 lines
    # Elevator drift report
    > code root: .
    > 3 cr reference(s): 2 resolved, 1 missing — 1 identifier(s) checked in 95 file(s), 0 unanchored
    
    ## Missing cr paths (error)
    > The code moved or was removed — fix the `cr:` or retire the entity.
    f parsers
      cr: docs/adr/0007-a-decision-that-was-deleted.md — not found under code root

    Captured by deliberately pointing a spec at an ADR that does not exist. Nothing about this output is written by hand.

Three layers, three decay rates

The ADR is immutable history and never rots. The graph is derived and cannot rot. The spec sits between them — the only authored layer, kept honest because every claim it makes about the code is a path or an identifier something can verify. How Elevator relates to ADRs, C4 and glossaries →

The visual lane

Filtering is the whole skill.

A graph view is only useful at the density you can read. The ladder below is the same in the VS Code extension, the browser UI and the demo on this site — go down it one rung at a time, and stop as soon as you can name what you're looking at.

  1. Scope before you look. Tick one folder, not the repo. Analysis can stay project-wide — it's the drawing that has to be narrow. In the extension these are two separate panels for exactly this reason.
  2. Aggregate up. Start at module or file level: one node per file, edges weighted by how many entity-level edges they stand for. Clusters and bridges are visible here and nowhere else.
  3. Drop to entities inside the scope. Now the names mean something, because you know which file they came from.
  4. Filter by kind and language. Hide parameters and imports; on a mixed codebase, look at one language at a time.
  5. Select, then follow. Selecting a node dims everything that isn't its neighbourhood — which is the visual form of impact.

The demo on this site compresses the first three rungs into one control — Landmarks / Structure / Everything — because a marketing page shouldn't ask you to learn a panel layout first. Try the ladder →

Every block above is reproducible.

Install nao, point it at your own repository, and run the same calls in the same order. The script that captured this page is in the site's source — no fixtures, no editing.