devscout-rs 0.6.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
Documentation
# Architecture

How `devscout` is put together: what each module owns, what it must keep true, and where a
change goes. Read this file before the code. `tools/check-architecture.sh` fails CI when a
module under `src/` has no row here or a row names a module that no longer exists, so the
table below is complete by construction.

## Pipeline

One `devscout map` run flows left to right; every query verb reads what it wrote.

```
walk ─► parse ─► extract ──┐
          │      markup ───┼─► resolve (C#) ─┐
          └── preproc      │   tsgraph (TS) ─┼─► graph ─► query ─► render ─► cli
                           └───────────────► manifest ─────────────────────┘
project ─► resolve        repo / initcmd / mapcmd / store / hookio / suggest / audit / offsets
```

- **Indexing** (`map`): `walk` lists files, `parse` and `preproc` turn C# into a syntax tree,
  `extract` and `markup` turn the tree into per-file fragments, `resolve` / `tsgraph` turn
  fragments into a graph, `graph` persists it, `manifest` persists the file-purpose index.
- **Querying** (`find`, `refs`, `read`, `impact`, `tests`): `query` builds an in-memory index
  over the persisted graph and answers; `render` formats; `cli` parses arguments and returns
  `(exit code, output)`.
- **Agent integration** (`init`, `hook`): `initcmd` installs hooks and registers the repo,
  `hookio` rewrites tool results, `store` keeps the freshness caches.
- **Measurement** (`audit --semantic`): `audit` scores `uses-member` edges against an
  oracle export.

## Modules

One row per file under `src/`. Invariants are the facts the rest of the crate relies on;
breaking one is a behaviour change even when every test still passes.

| Module | Responsibility | Invariants |
| --- | --- | --- |
| `src/lib.rs` | Crate root: declares every module and documents it in one line. | Every module under `src/` is declared here with a one-line doc. |
| `src/main.rs` | Binary entry point. | Hands `std::env::args()` to `cli::dispatch` and does nothing else. |
| `src/audit.rs` | `audit --semantic`: scores the graph's `uses-member` edges against an oracle's reference records, per tier, and evaluates `--assert` files. | Reads the graph read-only. `--assert` collects every violated or missing key before exiting non-zero. Oracle records are joined to edges by site (file and line), target and member; a `partial file mismatch` only affects the file join of true positives, never precision. |
| `src/cli.rs` | Root of CLI dispatch: the module overview, the `mod` list and the public re-exports. | Holds no code. The public surface is `dispatch`; `require_repo` stays `pub(crate)` so `audit.rs` can resolve its repo root the same way every command here does. |
| `src/cli/admin.rs` | The `map`, `stats` and `clear` commands: the index build, the cache-dedup summary, and the three prune forms. | None of the three read the graph. `clear`'s whole-store form never opens the store just to remove it; `--older-than` and `--session` are checked in that order, so the first one present wins when both are given. |
| `src/cli/answer.rs` | The answer shape every verb funnels through: the zero-hit exit code and its stderr note, the freshness warning, the two ambiguous-outcome renderers, the not-found fallback, and the one telemetry call a resolved query makes. | `EXIT_NO_RESULT` (3) is the one code that means a resolved, empty answer, distinct from failure (1) and a usage error (2). An ambiguous type and an ambiguous member answer under the same JSON keys from two different renderers, so a caller branching on `outcome` never has to tell the two apart. |
| `src/cli/args.rs` | Argument-parsing primitives shared by two or more verbs: the bare-query scan, `--pick`, the lenient `--hops`-style integer parse, `--no-guess`, and the `--json`/`--compact` conflict check. | `parse_int_js` accepts trailing garbage after the longest leading digit run (`"3abc"` parses to `3`); `parse_pick` does not -- a bad `--pick` value is always a usage error. `--no-dispatch` is parsed in `index_options` exactly the way `--no-guess` already is, on all four verbs. |
| `src/cli/coverage.rs` | The `tests` verb -- tests reaching a symbol. Named `coverage`, not `tests`, to avoid colliding with this module's own `#[cfg(test)] mod tests`. | Mirrors `refs`' own check order and ambiguity handling exactly; only the resolved arm's renderer and JSON builder differ. |
| `src/cli/dispatch.rs` | Top-level argument dispatch: `--help`/`--version`, the four dev/diagnostic subcommands, and wiring each verb's return value to stdout/stderr and the process exit code. | `init` dispatches through `initcmd::cmd_init_full`, never `cmd_init` directly. `hook read`/`hook bash` always exit 0 -- a broken scout must never break the tool call it is post-processing. |
| `src/cli/find.rs` | The `find` verb: the manifest search plus the declaration block a code or markup name resolves to. | The declaration and manifest pools are capped and ranked independently (`FIND_NAMES_CAP`, `FIND_FULL_CAP`, `FIND_FALLBACK_CAP`); tier 3 (a resource key) is a one-line trailer by default, inline only under `--resources`. |
| `src/cli/impact.rs` | The `impact` verb: its own numeric flags (`--hops`, `--iface-max-fanin`, `--hub-max-indegree`), a file-or-symbol seed rewritten repo-relative, and the blast-radius answer. | A resolved seed that reaches nothing beyond its own files is a zero hit, the same signal an unresolved seed gets. |
| `src/cli/import_edges.rs` | The `import-edges` verb: validates a cross-repo edge export and writes the auxiliary artifact `impact` reads. | `--repo <id>` is required and never inferred. A refusal (exit 1, naming the offending value) never calls the writer -- `write_imported_edges` is reached only once parsing already returned `Ok`. |
| `src/cli/read.rs` | The `read` verb: a declaration span layered on `refs`' own resolution. | Every outcome but the resolved one -- not-found, ambiguous, member-ambiguous, the bare-member answer -- is `refs`' own renderer, unchanged; only the resolved arm adds the span. |
| `src/cli/refs.rs` | The `refs` verb: usage text, argument parsing, and the outcome-to-answer mapping every result shape renders through. | `member_models_out` and `refs_model_row_count` are shared with `read`, whose own bare-member answer is refs' own. |
| `src/cli/root.rs` | Repo-root and graph-artifact resolution shared by every verb: the cwd a command runs from, the `.scout`/`.git` ancestor climb, the `-C` global flag, and the mapped `graph.json` every graph-reading verb needs. | `require_repo_for_path`'s argument-named-file fallback only applies once the caller's own directory has come up empty; `-C` composes on repeat exactly as git's does. |
| `src/cli/tests.rs` | Shared fixture builder for the CLI tests and the list of test topic modules. | `json_refs_model` is the only builder shared across more than one topic; everything else lives local to the topic file that uses it. |
| `src/cli/tests/admin.rs` | `js_math_round` and `js_float_string`'s rounding/formatting parity, and every `clear` form: whole-store, `--older-than`, `--session` (unique, ambiguous, no-match, missing-value), and the flag-precedence order. | Pins that a rejected `--older-than` value creates no store, and that `--older-than` wins when both flags are given. |
| `src/cli/tests/answer.rs` | The `J` JSON encoder's object/array whitespace and string escaping. | Pins the exact escapes and that no extra whitespace appears, matching `JSON.stringify`. |
| `src/cli/tests/args.rs` | `parse_int_js`'s lenient-integer parity with `parseInt`. | Pins that trailing non-digit garbage is dropped rather than rejected, and that a leading sign with no digits is `None`. |
| `src/cli/tests/coverage.rs` | `tests_model_to_json`'s `via` placement -- appended last and omitted on an attribute row -- and its `schema_version` lead. | Pins that a project-vouched row carries `via` but an attribute row carries none at all. |
| `src/cli/tests/find.rs` | `cmd_find`'s output caps, its zero-hit exit code, tier-1/2/3 handling, and the declaration-block-above-manifest-block ordering, built through the real extractor and resolver rather than hand-authored JSON. | Pins that a resource-key-only match brakes by default and that a manifest row with no declaration falls back to line 1. |
| `src/cli/tests/impact.rs` | `impact_model_to_json`'s key order: `heuristicCount` then `heuristic` after `score`, `tier` between `heuristic` and `ifaceVia`, and `heuristicAffected` after `manifestGap`. | Pins that every appended key is omitted, not merely empty, on a precise row. |
| `src/cli/tests/read.rs` | `read_model_to_json`'s `span` placement. | Pins that `span` sits immediately after `kind`, unmoved by the `schema_version` key gaining the lead. |
| `src/cli/tests/refs.rs` | `refs_model_to_json`'s and `member_refs_to_json`'s key order and omission rules: `heuristic`/`tier`/`source` appended last, the `outbound` table gated on `--out`, and the member wrapper's own envelope. | Pins that a precise row carries no trace of `tier` and that a member entry inside the wrapper is the resolved object unchanged. |
| `src/extract.rs` | Root of C# and TypeScript-family extraction: the `mod` list and the public re-exports. | Holds no code. The public surface is `extract`, `run_extract_dump`, `extraction_to_json`, `ts_extraction_to_json`, `extract_ts_file`, `extract_ts_fragment`, `TsFileExtraction`, the `TsFragment` record types, `compose_hybrid_ts_purpose`, `extract_ts_purpose`, `extract_ts_purpose_with_heuristic`, and the C# data model (`DefRecord`, `Extraction`, `ExtensionMethod`, `Fact`, `LambdaSlot`, `NameRecord`, `RefRecord`, `RegistrationRecord`, `UsingRecord`); every other item is `pub(super)` at most. |
| `src/extract/text.rs` | Tree-sitter/text primitives shared by the C# and TypeScript-family extraction: node text, declared names, namespace-level type discovery, and the truncated one-line purpose signature. | `truncate` counts UTF-16 code units, not bytes; `MAX_PURPOSE` (200) is the one limit both language extractors truncate to. |
| `src/extract/types.rs` | The C# data model extraction produces: `DefRecord`, `ExtensionMethod`, `RefRecord`, `RegistrationRecord`, `UsingRecord`, `Extraction`, `NameRecord`, `Fact`, `LambdaSlot`. | Plain data, no logic. A `Fact` carries the declared type name plus its top-level type arguments, and two facts agree only when both halves do. A `LambdaSlot` records where an untyped lambda sits so the resolver can type it. A `RegistrationRecord` carries its two type arguments' raw text verbatim; splitting a dotted name into a resolvable pair is the resolver's job. |
| `src/extract/refs.rs` | Building `RefRecord` candidates -- `push_ref`, `push_ctor_param_ref`, `push_member_ref`, `record_single_type`, `record_base_list` -- the type/generic-descriptor primitives that feed them, and `registration_fact`, the two-type-argument DI registration shape rule. | A member-access qualifier's `generic` flag is type-certainty syntax only a type can carry; `receiver_type` is set only for a bare, non-generic qualifier. `registration_fact` fires only for an invocation whose generic method name begins `Add`/`TryAdd` and ends `Singleton`/`Scoped`/`Transient` and carries exactly two type arguments; every other shape (one argument, a non-invoked method-group reference, an unmatched name) records nothing. |
| `src/extract/members.rs` | Raw per-member fact extraction for one type declaration: methods, properties, fields, bases, extension methods, arities, parameter descriptors, test methods, `override`-marked method names. | No accessibility filter on properties or fields; `methods` and `non_public_methods` partition method declarations with no overlap and no gap. `raw_override_method_names` draws from the same method population `raw_method_arities` does, so an override's own arity fact is read off that one table. |
| `src/extract/type_defs.rs` | Assembling `DefRecord`/`NameRecord`/`UsingRecord` instances from the raw member facts: `record_type_def`, `record_enum_members`, `record_declared_members`, `record_using`, `type_id`. | Nested-type ids join with `+`, never `.`; an enum member's id appends `.` even under a nested type. |
| `src/extract/receivers.rs` | The local/field fact tables (`FactTable`, `Fact`) a member-access qualifier resolves against: `collect_type_facts`, `collect_member_facts`, `visit_member_facts`. | The member table is flat and shadows the type table; a conflicting or untyped local is stored as `None` rather than omitted, so it never falls through to a same-named field. |
| `src/extract/lambdas.rs` | Untyped lambda-parameter slot typing: `lambda_argument_slot`, `lambda_slot_fact`, `lambda_receiver_element_fact`. | A lambda parameter's type is read positionally off its callee's delegate parameter, never off a fact table directly. |
| `src/extract/qualifiers.rs` | `Scope` and member-access qualifier resolution: `resolve_member_qualifier`, `chain_tail_receiver_type`, `resolve_call_chain_tail`. | `this`/`base` resolve against `type_stack`, never against a same-named local or field. A dotted qualifier never inherits a receiver fact. |
| `src/extract/walk.rs` | The top-level recursive-descent AST walk (`walk`, `walk_list`) and its `pub fn extract` entry point. | `type_stack` doubles as the "inside a type" signal; ref extraction runs at every depth regardless of nesting. A registration fact is checked at every `member_access_expression` that is actually invoked, independent of and alongside the uses-member/uses-type candidates the same call already builds. |
| `src/extract/dump.rs` | The `extract-dump` plumbing verb: `run_extract_dump`. | The only extraction code path that touches the filesystem; dispatches on extension to the C# or the TS-family extractor. |
| `src/extract/json.rs` | Hand-rolled JSON serialisation for both the C# and the TS-family fragment shapes: `extraction_to_json`, `ts_extraction_to_json`. | Matches `JSON.stringify(value, null, 2)` byte-for-byte; no serde dependency. |
| `src/extract/ts_purpose.rs` | TypeScript-family purpose composition: exported top-level declarations, CommonJS and ESM export shapes, re-export bucket segments. | Never builds a def/ref/usings candidate; purposes only. |
| `src/extract/ts_fragment_types.rs` | The TS-family fragment data model: `TsFragmentDef`, `TsBinding`, `TsImport`, `TsReexportName`, `TsReexport`, `TsRef`, `TsFragment`. | A TS fragment is tagged `ts: 1` so the resolver routes it away from the C# resolver; the two fragment shapes share no field beyond `defs`. |
| `src/extract/ts_fragment.rs` | TypeScript-family reference-fact extraction: imports, exports, calls, JSX uses, dispatching calls -- `extract_ts_fragment`, `extract_ts_file`. | Records only what a file says; resolving a specifier or a name to a declaration is `tsgraph.rs`'s job, not this one's. |
| `src/extract/tests.rs` | Shared fixture helpers for the extractor tests and the list of test modules. | `extract_src`/`find_def` are the shared entry points every topic file builds on. |
| `src/extract/tests/fqn.rs` | FQN building: dotted namespaces, `+`-joined nested-type chains, enum-member ids. | Pins that a `+`-joined id never collides with a literal dotted path. |
| `src/extract/tests/generic_arity.rs` | Declaration spans (`line`/`end_line`) and generic-arity refs from a type-argument list. | Pins that a nested type's span stays inside its own node. |
| `src/extract/tests/using_aliases.rs` | Using-directive parsing: alias, plain, global and static forms. | Pins that only an alias directive carries both a name and a target. |
| `src/extract/tests/qualifier_capture.rs` | Member-access qualifier text capture, deep chains, and `declaration_expression` out-declarations. | Pins that a deep chain flattens each window's own qualifier rather than inheriting one. |
| `src/extract/tests/misc_ref_edge_cases.rs` | Purpose-signature composition, `primary_constructor_base_type` inherits refs, tuple-type refs. | Pins the 200-UTF-16-unit purpose truncation. |
| `src/extract/tests/preproc_fluent_chain.rs` | `#if`/`#if-else` groups interrupting a fluent call chain. | Pins that a dead arm drops out without breaking the surviving chain. |
| `src/extract/tests/ts_purpose.rs` | TS/JS purpose composition acceptance cases, the `reexportBarrel.ts` fixture, the leading-comment hybrid purpose, and degrade paths. | Pins the hybrid purpose's comment-prefix composition and the zero-export/parse-failure degrade to `None`. |
| `src/extract/tests/preproc_duplicate_header.rs` | The duplicate-header preproc shape: nested defs and ascending line order across a blanked `#if` group. | Pins that defs and refs stay in ascending line order across the blanked arm. |
| `src/extract/tests/def_member_facts.rs` | Def-level member facts: properties, fields and method-return types recorded alongside `methods`. | Pins that a type declaring none of the new members still records all three as empty. |
| `src/extract/tests/receiver_facts.rs` | Receiver-fact construction: locals, `var`/`new`, parameters, fields, primary-ctor parameters. | Pins that `receiver_type` is set only on a ref that actually earned a fact. |
| `src/extract/tests/extension_method_facts.rs` | Extension-method def facts. | Pins source-order recording deduped by name, this-type and arity. |
| `src/extract/tests/arg_count_fact.rs` | The ref-side `argCount` fact on a member-access callee. | Pins that a chain ref never inherits the wrapping call's own arg count. |
| `src/extract/tests/test_methods_fact.rs` | `testMethods`: xUnit/NUnit/MSTest attribute recognition. | Pins that suffixed, qualified, targeted and shared-bracket attribute forms all match. |
| `src/extract/tests/outer_types_stack.rs` | The ref `outer_types` enclosing-type stack. | Pins outermost-first ordering and that a namespace-level ref carries none. |
| `src/extract/tests/names_list.rs` | The per-file `names` list. | Pins that a `names` line is the name token's own row, not the attribute line. |
| `src/extract/tests/ts_ref_fragment.rs` | The TS/TSX reference fragment: imports, exports, calls, JSX uses. | Pins default/named/aliased/namespace import-binding recording. |
| `src/extract/tests/property_types_fact.rs` | Property types and `var`-from-invocation call facts. | Pins that `receiverPropertyOwner` is recorded only for a typed two-segment chain. |
| `src/extract/tests/foreach_element_fact.rs` | `foreach` element-type facts over a generic collection. | Pins that a foreach variable used as another foreach's collection is never a fact. |
| `src/extract/tests/stage4_receivers.rs` | `this`/`base`/conditional receivers and await/cast/pattern facts. | Pins that a `base.` qualifier starts lookup at the bases, never the enclosing type. |
| `src/extract/tests/chain_tail_and_lambda_element.rs` | Chain-tail receivers (`a.B().C()`) and lambda-parameter element typing. | Pins that a second hop of a chain emits no ref. |
| `src/extract/tests/method_params_fact.rs` | `method_params`: per-overload parameter descriptors. | Pins that a type parameter is recorded as a wildcard and a delegate's parameters land under `Invoke`. |
| `src/extract/tests/lambda_slot_fact.rs` | `LambdaSlot`: the site-side untyped-lambda callee slot. | Pins that the collection-element rule wins over a lambda slot when both shapes match. |
| `src/extract/tests/registration_fact.rs` | The two-type-argument DI registration shape rule. | Pins the `Add`/`TryAdd`-prefix, `Singleton`/`Scoped`/`Transient`-suffix rule against keyed and named spellings, and that a one-type-argument or non-invoked form records nothing. |
| `src/graph.rs` | Root of the persisted graph layer: the schema notes in its header, the `mod` list and the public re-exports. | Holds no code. Every public item keeps the path it had before the split; everything else is `pub(crate)` at most. |
| `src/graph/artifact.rs` | The `Graph` artifact itself: `Stats`, `GraphName`, `GraphUnit`, `GRAPH_SCHEMA_VERSION`, and `read_graph`/`write_graph`. | `schema_version` is the first key written and the one `rebuild_graph` checks before reusing an artifact; `names` then `units` are appended last, each omitted entirely when empty rather than written as `[]`. |
| `src/graph/cache.rs` | The fragments cache: `FragmentCacheEntry`, its read/write pair, the mtime-only index (`read_fragments_index`/`write_fragments_index`), and `index_is_stale`. | The index is stale on any mtime mismatch or a differing file count; a per-file cache hit is keyed on mtime equality, not a content hash. |
| `src/graph/def.rs` | The persisted def row: `Def` and its `also_in` partial-class sidecar, `AlsoIn`. | A partial class's later declaring sites land in `also_in`, never as a second top-level def; `also_in` and `end_line` are both omitted when empty/zero, not written as `[]`/`0`. |
| `src/graph/edge.rs` | The persisted edge shapes: `Edge`'s kinds, `Candidate`, `HeuristicTier`, and the two edge-count summaries `EdgesByKind`/`HeuristicByTier`. | Edges are shape-heterogeneous by `kind`, tagged first; `heuristic` is appended last and omitted when false, and a `uses-member` edge's `tier`/`member` pair is present exactly when `heuristic` is. `Edge::Implements`/`Edge::Overrides` never carry a heuristic tier; `member` is absent on a type-level `implements` edge and present, naming the satisfied or overridden member, on a member-level one. `EdgesByKind` counts both kinds after `ctor-di`, before the optional TS keys. |
| `src/graph/fragment.rs` | Building a fragment from an extractor `Extraction` (`fragment_from_extraction`) and from a markup/resource file's line scan (`markup_fragment`). | `fragment_from_extraction` is graph-only -- `Extraction.purpose` has no fragment counterpart. A markup file's ref carries an empty (not absent) enclosing namespace, the same shape a file-scope C# ref carries. |
| `src/graph/fragment_types.rs` | Every serde struct for a raw per-file fragment: `FragDef`, `FragRef`, `FragUsing`, `FragName`, `Fragment`, and `AnyFragment`, the untagged union of a C# and a TypeScript fragment. | `AnyFragment`'s two arms are mutually exclusive by field shape (a TS fragment carries `ts`, a C# one never does), so a fragment cache file can hold both without a discriminant key; every added field on `FragDef`/`FragRef` is appended last and omitted when absent. `FragDef.override_methods` and `Fragment.registrations` are both purely additive and omitted when empty, so a fragment written before either existed still deserializes. |
| `src/graph/imports.rs` | The imported-edges artifact: `ImportedEdges`, `ImportRecord`, `EdgeEnd`, `Provenance`, `parse_imported_edges`, `read_imported_edges`/`write_imported_edges`. | Validation is total before any write: a format/schema-version mismatch, a record missing `kind`/`from`/`to`, or a kind outside the fixed set names the offending value and writes nothing, so a refusal never touches whatever artifact already existed. A re-import always overwrites the file wholesale; there is no merge. |
| `src/graph/ordered.rs` | `OrderedMap`, the insertion-order-preserving map backing every persisted `{rel: ...}` object, and `Percent1`, the whole-or-one-decimal percentage the audit stats print. | A `HashMap` has no serialization order and serde_json's default map sorts by key; neither matches graph.json's file-walk order, so both need the hand-rolled `Serialize`/`Deserialize` here. `Percent1` prints without a decimal point when the tenths value is a whole ten. |
| `src/graph/paths.rs` | Graph-directory and artifact-path resolution, the superseded fragment-cache sweep, and the atomic (tmp-then-rename) write helpers every persisted file goes through. | The def/ref graph and its caches live under the git common directory when there is one, `<root>/.scout/graph/` otherwise; a write's tmp file and its target share a directory so the rename is same-filesystem and atomic. The fragment cache filename carries the fragment schema version; bump it whenever the extractor records something old fragments lack, and delete every superseded generation. |
| `src/graph/rebuild.rs` | `devscout map`'s cache-then-resolve-then-write cycle: `GraphFile`, `RebuildOutcome`, and `rebuild_graph` itself. | The unchanged fast path is gated on both `changed == false` and the on-disk artifact already carrying the current schema version; a cache hit reuses a fragment by matching mtime even when the caller's `fresh_fragments` is empty. |
| `src/graph/tests.rs` | Shared fixture builders for the graph tests and the list of test modules. | `temp_dir` is the only builder shared across more than one topic; everything else lives local to the topic file that uses it. |
| `src/graph/tests/artifact.rs` | `Stats`'s appended tail counters and `Graph`'s `names`/`units` append-last shape. | Pins that `heuristic_by_tier` stays last even when a TS `stats.ts` block is present, and that an empty `units` list writes no `units` key at all. |
| `src/graph/tests/cache.rs` | The fragments-cache generation bump: superseded-file cleanup and the current cache/index path pair. | Pins that every superseded generation is deleted on a rebuild and that the current pair is named for the fragment schema version. |
| `src/graph/tests/def.rs` | `Def`'s `also_in`/`end_line` omission and the test-coverage `testMethods` slot. | Pins that `testMethods` sits between `methods` and `also_in`, and that either is omitted entirely when the def carries none. |
| `src/graph/tests/edge.rs` | `Edge`'s per-kind shape and `EdgesByKind`'s fixed field order. | Pins that `heuristic` is omitted on a precise edge and that `edges_by_kind` never serializes alphabetically. |
| `src/graph/tests/fragment_types.rs` | `FragDef`/`FragRef`'s appended-field order and omission-when-empty, and the `AnyFragment` untagged round trip. | Pins each field's exact byte position and that a fragment cached before a field existed still deserializes with it defaulted. |
| `src/graph/tests/imports.rs` | `parse_imported_edges`'s acceptance and refusal shapes, plus the artifact's write/read round trip. | Pins that a format or schema-version mismatch, a record missing `kind`/`from`/`to`, and a kind outside the closed set each name the offending value; pins that a missing artifact reads as `None`, never an error. |
| `src/graph/tests/ordered.rs` | `OrderedMap` and `Percent1` serialization. | Pins insertion order (not sorted) through a JSON round trip and that `Percent1` prints a whole value without a decimal point. |
| `src/graph/tests/paths.rs` | The atomic write helper under a simulated crash. | Pins that a stray leftover tmp file never corrupts the target and that no tmp file survives a clean write. |
| `src/graph/tests/rebuild.rs` | `rebuild_graph`'s unchanged-path skip, its old-schema and truncated-artifact rebuild triggers, and per-file cache reuse. | Pins that `changed == false` alone is not enough to skip a rebuild when the on-disk schema is stale. |
| `src/hookio.rs` | `hook read` and `hook bash`: rewrite a tool result on stdin when the same content was already delivered in the session. | Fails open: malformed, non-UTF-8 or incomplete input yields empty output and never an error. Only these hooks write the freshness stores; plain CLI use never touches them. |
| `src/initcmd.rs` | `init`: registers the root, creates the artifact directory, installs the agent hooks into the settings file, runs a first map. | Merges hook entries into an existing settings file after backing it up; a hook-install failure never fails `init`. `--no-hooks` skips the install and nothing else. Adds `.scout` to the local git exclude file. |
| `src/manifest.rs` | `manifest.json` read/write, `find` search, `index-state.json` and the query-time freshness warning. | `find` ANDs whitespace-split lowercase tokens over `path + purpose` and falls back to any-token matches ranked by hit count. A missing or corrupt `index-state.json` reads as absent, never as an error. The freshness warning costs one `git rev-parse HEAD` and one scoped `git status --porcelain`, the same two calls `map` pays. |
| `src/mapcmd.rs` | `map`: assembles walk, extract, resolve, graph and manifest into one incremental run. | Unchanged files are reused from the fragment cache (content hash by default, mtime under `SCOUT_MTIME_REUSE=1`). The unchanged path opens no graph file. A `.csproj` edit is detected through the project-units sidecar, not through the fragment index. |
| `src/markup.rs` | XAML, RESW and RESX extraction: `x:Class` definitions, element references and resource keys. | A markup `x:Class` def carries the same id as its code-behind `partial class`, so the two merge into one symbol with two declaring sites. |
| `src/offsets.rs` | UTF-8 byte offset to UTF-16 code-unit offset table. | Off every code path today; kept unit-tested for a caller that must cross a UTF-8 buffer back into the UTF-16 offset convention `parse` uses. |
| `src/parse.rs` | Tree-sitter parsing for C# and the TypeScript-family grammars; span collection; the `parse` and `spans` plumbing verbs. | Source is fed to the grammar as UTF-16 code units, so every node offset the crate sees is already UTF-16. Grammar crates are pinned to exact versions in `Cargo.toml`, and `docs/csharp-coverage.md` names the pin its tables were checked against. |
| `src/preproc.rs` | C# conditional-compilation pre-pass: blanks inactive `#if` / `#elif` / `#else` arms. | Evaluates with no predefined symbols. Inactive bytes become spaces and every line break stays where it was, so line and column of surviving code do not move. |
| `src/project.rs` | Hand-scanned `.csproj` and `Directory.Build.props` model: discovery, project units, reference closure, test-project detection. | Discovery never fails because nothing was found; zero projects is `Ok(None)`. The sidecar comparison is a raw byte compare against `project-units.json`. |
| `src/query.rs` | Root of the query layer: the verb/substrate map in its header, the `mod` list and the public re-exports. | Holds no code. Every public item keeps the path it had before the split; everything else is `pub(super)` at most. |
| `src/query/dispatch.rs` | `implements`/`overrides` plumbing shared by `index.rs` and `refs.rs`: `record_dispatch_edge` (the index-build-time adjacency push), `implemented_interfaces` (the interface hop's own `inherits`-plus-`implements` widening), the bare-member fallback's exact-match half (`member_dispatch_edges`, `dispatch_table`, `split_dispatch_rows`), and the outbound kind indices and ranking helpers (`RankedOutbound`, `outbound_foreign`, `push_ranked`). | Split out of `index.rs`/`refs.rs` to keep both under this crate's per-file line budget, not a verb of its own. `member_dispatch_edges` verifies by an exact match on the edge's own `member` field, never `line_has_token`. |
| `src/query/coverage.rs` | The tests-reaching-a-symbol verb (named to avoid colliding with this module's own `tests`): `build_tests_model`, `TestsModel`, `TestVia`. | A file's own attributed test def outranks the project-model vouch; a symbol nothing tests answers "none", never a name-convention guess. A member seed (via `member.rs`), tried only once the type ladder has missed, answers as its unique declaring type; two or more declaring types answers `MemberAmbiguous`. `implements`/`overrides` join the inbound kinds `tests` folds through, gated by `--no-dispatch` at index build time like every other consumer. |
| `src/query/find.rs` | The `find` verb: `find_names`, `first_decl_line_by_file`, `file_inbound_counts`, `name_tier`, `source_line`. | `file_inbound_counts` excludes heuristic edges, `imports`, `ctor-di`, `implements`, `overrides`, `ambiguous`, and a file's references to itself. |
| `src/query/impact.rs` | The `impact` verb: `impact_walk`'s reverse k-hop traversal, the interface hop and its fan-in brake, the hub brake, `resolve_impact_seed`, `build_impact_model`. | Records a file once, at its minimum hop. A heuristic edge reaches a file and stops there -- it never widens the walk further. A member seed (via `member.rs`) answers as its unique declaring type, tried only once the file-path check and the type ladder have both missed; two or more declaring types answers `MemberAmbiguous`. `implements`/`overrides` join the plain inbound walk directly, and `implemented_interfaces` (in `dispatch.rs`) folds `implements` in beside `inherits` for the interface hop, so a registration-driven implementation widens exactly the way a base-list one already did. |
| `src/query/impact_why.rs` | An `ImpactRow`'s `why`: `Hit`'s `note_direct`/`note_direct_amb`/`note_iface`/`note_heuristic` methods, each pairing a `KindLines` slot with the edge that set it, and `primary_why`/`build_visited_entry`, which fold that pairing into one value per file. Split out of `impact.rs` to keep that file under its size ceiling. | The strongest evidence wins when more than one edge kind reached a file: an explicit `ctor-di` site over a plain resolved reference, over the broader interface hop, over a guess. |
| `src/query/imported.rs` | Imported-edge reach for `impact`: `ImportedRow`, `ImportedSection`, `build_imported_section`. Split out of `impact.rs`, which has no room left under its own size ceiling. | A lookup over the native walk's already-finished `ImpactModel`, never a widening of `impact_walk` itself: an imported edge reaches a file and stops there, the same rule a heuristic edge already follows, so native rows are unaffected by an import's presence. Capped and counted apart from the native rows. |
| `src/query/index.rs` | `GraphIndex` and its two-phase load (`load_graph_index`, `load_graph_index_with`), `IndexOptions`, plus the def-site helpers `def_files`/`def_sites`/`symbol_refs`. | Heuristic edges are indexed in their own adjacency, never mixed into the precise one; `--no-guess` admits only the extension tier. A corrupt manifest.json fails open. `implements`/`overrides` are recorded against the IMPLEMENTATION or `override` type's own declaring file and line -- never a registration call site -- so they join `inbound`/`outbound_by_file` through the same file attribution `inherits` already uses; `IndexOptions.include_dispatch` (`--no-dispatch`) admits neither kind into either adjacency when `false`, and neither kind ever carries a heuristic tier. |
| `src/query/infra.rs` | The hub-file name-pattern classification `impact` widens against: `is_infra_file`, `DEFAULT_HUB_MAX_INDEGREE`. | A classification of what a file is FOR, always on; the in-degree half of the hub brake is a separate, disable-able threshold. |
| `src/query/json.rs` | `--json` rendering of every query model (the hand-rolled `J` value tree and its encoder, `refs_model_to_json`, `read_model_to_json`, `member_refs_to_json`, `impact_model_to_json`, `tests_model_to_json`), moved out of `cli.rs` to keep that file under its size ratchet. | `pub(crate)`, called from the matching `src/cli/*.rs` file as `query::json::...`; `audit.rs` reuses the same `J` tree. Key order is part of the contract -- see each builder's own doc comment. Every builder leads with a top-level `schema_version` and appends a top-level `outcome` (`query::Outcome`) last; every hit row carries a `why` (`query::Why`), derived from the row's own kind and, for `uses-member`, its tier. The `implements`/`overrides` keys are present only when that table's own total is non-zero, the same present-only-when-applicable rule `memberRefs` already follows. |
| `src/query/member.rs` | Member-seed resolution shared by `refs`/`read`/`impact`/`tests`: `split_member_seed`, `qualified_member_owners`, `resolve_member_seed`, `MemberSeedResolution`, `MemberCandidate`, `qualified_seed` (the string `--pick` re-resolves against). | Reached only once `resolve_symbol`'s type ladder has already missed. A qualifier (`Type.Member`/`Namespace.Type.Member`) admits an owner by exact id or dotted suffix, the same tail rule the type ladder's own dotted step uses. |
| `src/query/outcome.rs` | The closed `--json` outcome vocabulary every `refs`/`read`/`impact`/`tests` answer carries: `Outcome` (`hit`/`zero-hit`/`ambiguous`/`fallback-advised`), and `SCHEMA_VERSION`, the answer shape's own version number. | The only place any of the four outcome words is spelled; no file under `src/cli/` ever writes one as a literal string. `SCHEMA_VERSION` bumps only when an existing key is renamed, removed, or given a different meaning -- never for an added key. |
| `src/query/rank.rs` | Personalized PageRank over a file-level subgraph: `personalized_page_rank`, `DEFAULT_DAMPING`, `DEFAULT_ITERATIONS`. | Ranking only, never removes a node; callers must build the `nodes` array in a fixed order for a bit-stable result. |
| `src/query/read.rs` | The `read` verb: `build_read_model`, the declaration span (`ReadSpan`). | Reuses `refs` resolution and inbound machinery wholesale with `out` off; a span past EOF or before a shrunk file's start degrades to no span, never invented text. |
| `src/query/refs.rs` | The `refs` verb: `build_refs_model`, the member-seed fallback (bare, `Type.Member`, `Namespace.Type.Member`, via `member.rs`), enum member-ref rollup, ranked inbound/outbound capping. | The member path is reached only when nothing declares the query as a type. A member hit is verified by a whole-token match on its referencing line; more than one declaring type surviving that verification answers `MemberAmbiguous`, never the bare type list `Ambiguous` renders. `implements`/`overrides` share the one inbound cap, but never share a table header: `refs`/`read` print either only when its own total is non-zero, unlike the first three. |
| `src/query/refs_tables.rs` | The row/table shaping `refs`, `read` and `impact` all reuse: `Table`, `InboundRow`/`OutboundRow`/`ImportRow`/`AmbiguousRow`, the row caps (`DEFAULT_CAP`, `INBOUND_CAP`, `OUTBOUND_CAP`, `SOURCE_MAX`). | Location sorts use plain `str::cmp`; `row_tier` never reports a stronger tier than the edges behind a row actually prove. `InboundTables`/`OutboundTables` carry `implements`/`overrides` alongside `inherits`/`uses-type`/`uses-member`; `edge_loc` covers both new kinds the same way it covers the first three. |
| `src/query/seq.rs` | Insertion-order-preserving scratch structures: `SeqSet`, `SeqMap`. | First insertion wins a slot; neither type is a persisted artifact shape. |
| `src/query/symbol.rs` | Symbol resolution: `resolve_symbol`'s never-guess ladder and `Resolution`. | Exact id, then unique exact name, then unique case-insensitive name, then a unique dotted-id tail; two or more candidates at any step is ambiguous, never resolved further. |
| `src/query/tests.rs` | Shared fixture builders for the query tests and the list of test modules. | Fixtures build `graph::Graph` directly, no JSON round-trip; a temp `.git`-shaped dir backs the one manifest fixture that needs disk. |
| `src/query/tests/coverage.rs` | `build_tests_model`, the attribute and project-model vouches. | Pins that a harness file in a test project earns a row with no test defs of its own. |
| `src/query/tests/dispatch.rs` | `implements`/`overrides` in `refs`, `impact`'s widened interface hop, `tests`, and `--no-dispatch`. | Pins that `--no-dispatch` removes both the inbound table and the interface-hop widening a registration-driven `implements` edge provides, with no effect on an ordinary base-list `inherits` edge. |
| `src/query/tests/find.rs` | `find_names`, `file_inbound_counts`, `first_decl_line_by_file`. | Pins which edge kinds count as inbound interest and that a file's self-references never do. |
| `src/query/tests/heuristic_ordering.rs` | Heuristic-tier row ordering shared by `refs` and `impact`. | Pins that every precise row sorts before any heuristic row and that a full cap of precise rows leaves no room for guesses. |
| `src/query/tests/impact_from_lines.rs` | The per-edge-kind referencing line on an impact row. | Pins the lowest-line-per-kind rule and that an ambiguous-only hit still names its line under `direct`. |
| `src/query/tests/impact_hub.rs` | The hub-file brake: in-degree and `is_infra_file`'s name patterns. | Pins the four `is_infra_file` shapes and that a hub reached on the last hop is never reported as braked. |
| `src/query/tests/impact_iface.rs` | The ctor-di and interface-hop widening path. | Pins that an ambiguous ctor-di edge and a same-named foreign interface never widen the walk. |
| `src/query/tests/impact_iface_brake.rs` | The broad-interface fan-in brake. | Pins that the brake narrows only the braked interface's own hop, never a narrower sibling's. |
| `src/query/tests/impact_misc.rs` | Enum-reached files, `uses-member` blast radius, PPR sanity, seed-argument parsing. | Pins that `looks_like_file_path` matches the same shape a `.`-suffixed symbol id would. |
| `src/query/tests/impact_ranking.rs` | `build_impact_model`'s hop limit, file-path seeding, PageRank-ordered ranking. | Pins that ranking is deterministic across repeated runs and never drops a row below the cap. |
| `src/query/tests/imported.rs` | `build_imported_section`'s direct and composed reach, the mapped-repo guard, the message-key join, and its own cap/drop accounting. | Pins that a foreign edge landing on a native row reports one hop further out, that duplicate records landing on the same foreign file collapse to one row, and that the composed join never echoes the publisher's own arriving site back as its consumer. |
| `src/query/tests/index.rs` | `load_graph_index`'s manifest join, hub in-degree, `--no-guess`, the heuristic adjacency. | Pins that a guess never enters the precise adjacency and that `--no-guess` keeps the extension tier only. |
| `src/query/tests/member.rs` | Member-seed resolution across `impact`/`tests` (bare, `Type.Member`, `Namespace.Type.Member`), and the type-ladder-first order shared with `refs`/`read`. | Pins that a name resolving to a type never falls through to the member ladder, and that a member ambiguity answers candidate rows, never a bare type list. |
| `src/query/tests/read.rs` | `build_read_model`'s declaration span. | Pins that a reference inside the declaration's own span is excluded from its inbound count. |
| `src/query/tests/refs_bare_member.rs` | The bare-member fallback: `line_has_token`, verified single- and multi-owner lookups. | Pins that a longer identifier sharing the query's prefix is refused and that a type answers before a same-named member. |
| `src/query/tests/refs_enum.rs` | `build_refs_model` on an enum and its members. | Pins that a member's inbound edges union into the enum's own answer and that two same-named members stay ambiguous. |
| `src/query/tests/refs_tables.rs` | `build_refs_model`'s inbound/outbound/ambiguous tables, caps and ranking. | Pins the rank order (resolved before heuristic, own project before foreign) and that `--all` lifts both caps together. |
| `src/query/tests/symbol.rs` | `resolve_symbol`'s ladder, including the enum-member tail. | Pins the exact-id/unique-name/case-insensitive/dotted-tail order and that two candidates at any step stay ambiguous. |
| `src/query/why.rs` | The closed `why` vocabulary every hit row on `refs`/`read`/`impact`/`tests` carries in `--json`: `Why`, and `why_for_edge`/`why_for_uses_member`, which derive it from an edge's kind and (for `uses-member`) its tier. | The only place any of its words is spelled. A TS/TSX-only edge kind (`import`/`call`/`jsx-use`/`dispatch`) names none of them: `query::index` never admits those kinds into the adjacency any of the four verbs read, so a row built from one never exists for `why` to describe. `implements` and `overrides` are appended after the words that pre-date the two edge kinds, so no existing word moves. |
| `src/render.rs` | Root of the render layer: the module overview, the `mod` list and the public re-exports. | Holds no code. Every public item keeps the path it had before the split; everything else is `pub(crate)` at most. |
| `src/render/blocks.rs` | The table-to-lines primitives every renderer builds on: `ref_kind_block`, `compact_block`, `group_by_file`, `rle`. | `ref_kind_block` prints its header whenever the table is present even at `total == 0`; `compact_block` additionally suppresses on a zero-total table. Both accept a missing (`None`) table, a path only the tests exercise directly. `ref_kind_block_if_any` prints a header only when the table it is given carries at least one row, unlike `ref_kind_block`, which prints one for a present-but-empty table. |
| `src/render/coverage.rs` | The `tests` verb's renderers: `render_tests_text`, `render_tests_compact`. | The zero-hit case is one line, never a header over an empty table; heuristic files carry the same word/character suffix the refs and impact renderers use, and the summary counts stay precise-only. |
| `src/render/impact.rs` | The `impact` verb's renderers: `render_impact_text`, `render_impact_compact`. | The `affected` count is precise-only; a heuristic-reached file's `via` reports its guess count, never the zero `via_count` a heuristic-only row carries by definition. |
| `src/render/markers.rs` | The heuristic/guess suffix vocabulary shared by every renderer: `heuristic_suffix`, `compact_marker`, `test_via_suffix`, `source_suffix`, `seed_kind_str`. | `heuristic_suffix`/`compact_marker` read `tier` only when `heuristic` is set; an extension and a scored guess get their own word (`(extension)`/`(guess)`, `x`/`h`), never the umbrella `(heuristic)`, once a tier is known. |
| `src/render/read.rs` | The `read` verb's renderers: `render_read_text`, `render_read_compact`. | Reuses `refs`'s own renderers for the inbound section wholesale, only replacing the `def:` line with the declaration span. |
| `src/render/refs.rs` | The `refs` verb's renderers: `render_refs_text`, `render_refs_compact`, and the enum member-count line. | The member-count line prints only when the model carries at least one member-level reference; an imports row never carries a heuristic suffix, having no flag to read. The default renderer prints an `implements`/`overrides` header only when that table's own total is non-zero, so a symbol untouched by dispatch edges renders byte-identical to before the pair existed; `--compact`'s blocks already self-skip an empty table for every kind. |
| `src/render/tests.rs` | Shared fixture builders for the render tests and the list of test modules. | `table` is the only builder shared across more than one topic; everything else lives local to the topic file that uses it. |
| `src/render/tests/blocks.rs` | `ref_kind_block`/`compact_block`'s missing- and empty-table tolerance, and `rle`. | Pins the asymmetry: a present-but-empty table still prints its header under `ref_kind_block`, but not under `compact_block`. |
| `src/render/tests/coverage.rs` | `render_tests_text`/`render_tests_compact`'s zero-case, heuristic-suffix and test-project-vouch shapes. | Pins that a project-vouched row prints its lines with no def id and that the header counts stay precise-only. |
| `src/render/tests/impact.rs` | `render_impact_text`/`render_impact_compact`'s heuristic/tier suffixes, hop grouping and the tests-affected summary term. | Pins that a heuristic-only row's `via` reports its guess count, and that `tests=` joins the summary only when the blast radius actually reaches a test file. |
| `src/render/tests/markers.rs` | `seed_kind_str`'s two string literals. | Pins the exact strings the `impact` JSON and CLI error messages key on. |
| `src/render/tests/refs.rs` | `render_refs_text`/`render_refs_compact`'s per-kind headers, RLE grouping, the member-count line and the heuristic/tier suffixes. | Pins that a kind with zero rows prints no header in `--compact` and that two tiers off one line number stay distinct RLE entries. |
| `src/repo.rs` | Root discovery (`.scout` ancestor, git worktree, git common dir) and the registry file. | Root discovery returns absolute, normalised paths. A missing registry reads as empty; a corrupt one is an error, so a lost registry is never mistaken for an empty one. |
| `src/resolve.rs` | Root of C# resolution: the ladder rules in its header, the `mod` list and the public re-exports. | Holds no code. The public surface is `resolve_graph`, `resolve_graph_with_ts`, `resolve_graph_with_model`, `DefIndex`, `ExtCandidate`, `MemberLists`, `MethodOverloadParams`; every other item is `pub(super)` at most. |
| `src/resolve/index.rs` | The def index: `DefIndex`, `MemberLists`, `ExtCandidate`, `MethodOverloadParams`, `build_def_index`, `name_probe`. | Defs keep first-insertion order; a partial class's later files land in `also_in`, never as a second def. Enum members are keyed by qualified name but excluded from the simple-name pool. `MemberLists.override_methods` is merged across a partial class the same way `non_public_methods` is, a plain union in first-insertion order. |
| `src/resolve/arity.rs` | Arity admission for instance overloads and extension entries, including generic-argument unification. | An optional parameter makes an entry a range and a `params` array accepts any count; concrete type arguments must match, a wildcard unifies with an unbound method type parameter. |
| `src/resolve/members.rs` | Member declaration checks (`declares_member`, `member_shape`, `member_vouched`) and the inheritance-closure walk that finds the base declaring a member. | The base walk visits class bases in declaration order before any interface, terminates on a cycle, and never binds a class-typed receiver to an interface declaration. A scored guess never vouches through a non-public member. `first_base_declaring` is `pub(super)` so `dispatch.rs` can reuse the SAME nearest-base search for an `overrides` edge's own base lookup. `first_base_declaring` is `pub(super)` so `dispatch.rs` can reuse the SAME nearest-base search for an `overrides` edge's own base lookup. |
| `src/resolve/scope.rs` | Per-file scope (`FileContext`: namespace, usings, aliases), candidate scoring, and global usings per project unit. | Global usings scope to the declaring unit when a project model exists, are repo-wide without one, and fall open for a file no project owns. An empty namespace is absent, not a matchable prefix. |
| `src/resolve/receiver.rs` | Receiver typing: field and property types, property and call-return hops, delegate and lambda slots, descriptor unification, the assignability cache, candidate admission, nested-type visibility. | A lambda parameter is typed positionally from the callee's delegate parameter and stays untyped when overloads disagree. An external receiver refuses a candidate not assignable to it. A chain tail whose hop fails emits no guess. |
| `src/resolve/ladder.rs` | The ladder itself: `resolve_ref` steps 0 to 4, dotted-suffix and nested-qualifier fallbacks, project-reachability narrowing (`Admission`, `narrow_by_reachability`), `capped_candidates`. | A step that finds exactly one candidate resolves; two or more stop as ambiguous. Ambiguous lists are capped at five and sorted by id. A dotted reference the exact step misses never reaches steps 2 to 4. |
| `src/resolve/edges.rs` | Edge construction (`type_edge`, the heuristic dedup key) and constructor-injection resolution (`resolve_ctor_param`, the implementor index, infrastructure-namespace detection). | `ctor-di` binds a sole implementor, prefers a closed implementor over an open generic one, and records a tie as ambiguous. Byte-identical guesses collapse to one edge. `type_probe` builds the bare-or-dotted synthetic type reference `dispatch.rs` resolves a registration's own type argument through. `type_probe` builds the bare-or-dotted synthetic type reference `dispatch.rs` resolves a registration's own type argument through. |
| `src/resolve/dispatch.rs` | Dispatch edges: `resolve_registrations` (the type-level `implements` edge from a two-type-argument DI registration) and `resolve_member_dispatch` (member-level `implements`/`overrides`, scoped to exactly the implementation types a registration resolved). | A registration whose service or implementation type does not resolve to exactly one in-graph def emits no edge. Both edge kinds are recorded against the IMPLEMENTATION type's own declaring file and line, never the registration call site, so they reuse `inherits`'s own file-attribution with no new index. `matching_overload_count`'s pair count is the one arity-uniqueness test both `implements` and `overrides` use: exactly 1 resolves, 0 or 2-or-more emits nothing. `overrides` is computed once per implementation type, not once per registration. A repository with no registration facts runs both passes over an empty list. |
| `src/resolve/assembly.rs` | `resolve_graph`, `resolve_graph_with_ts`, `resolve_graph_with_model`: the pass over every file's fragments, the `Ext` and `Guess` tiers, stats. | A ref an earlier tier answered never gets a heuristic duplicate. The scored tier refuses a member name carried by more than `SCORED_UNIQUENESS_CAP` defs and emits at most `SCORED_EMIT_CAP` edges. A `None` project model leaves the graph byte-identical. `dispatch::resolve_registrations`/`resolve_member_dispatch` run once, after the main ref loop; both count under `edges_by_kind.implements`/`.overrides` regardless of which pass emitted the edge. |
| `src/resolve/tests.rs` | Shared fixture builders for the resolver tests and the list of test modules. | Fragments are built by hand, never parsed; `no_git_root` keeps `built_at_head` at `None`. |
| `src/resolve/tests/ladder.rs` | Head hash, aliases, ambiguity caps, enum-member ids. | Pins the alias short-circuit and the five-entry ambiguous cap. |
| `src/resolve/tests/member_qualifiers.rs` | `uses-member` qualifier rules and declaration-expression type refs. | Pins when a bare or dotted qualifier emits and when it is dropped silently. |
| `src/resolve/tests/qualified_names.rs` | Enclosing-namespace prefixes and foreign-qualified names. | Pins that a foreign-qualified name never binds a same-named in-tree type. |
| `src/resolve/tests/dotted_suffix.rs` | Dotted-suffix matches, global aliases, nested types through a derived type, same-namespace and global-using resolution. | Pins that two path-suffix matches stay ambiguous. |
| `src/resolve/tests/receiver_tier.rs` | The static-qualifier and declared-receiver tiers. | Pins that a receiver whose type does not declare the member earns no edge. |
| `src/resolve/tests/nested_end_to_end.rs` | Multi-level nested qualifiers and instance receivers named like a type. | Pins that a namespace type never shadows a same-named nested type. |
| `src/resolve/tests/extension_tier.rs` | Extension-method admission: namespaces, ranges, vetoes by declared members. | Pins that an instance member shadows a visible extension of the same name. |
| `src/resolve/tests/extension_generics.rs` | Generic `this` parameters against concrete and wildcard receivers. | Pins exact concrete-argument matching. |
| `src/resolve/tests/scored_tier.rs` | The scored guess tier and the member-shape rule. | Pins the uniqueness and emit caps and the emitted order. |
| `src/resolve/tests/receiver_rule.rs` | External-receiver assignability and generic-sibling selection. | Pins that a generic receiver binds the generic sibling, not the first indexed def. |
| `src/resolve/tests/byte_identity.rs` | Serialization stability of the edge array with and without heuristics or a project model. | Pins the byte-identity gate the moves are held to. |
| `src/resolve/tests/project_admission.rs` | Project reachability: admission, unit-scoped global usings, narrowing. | Pins that a non-test site never names a def in a test project. |
| `src/resolve/tests/graph_invariants.rs` | Edge schema, stats, partial-class merging, dedup, counts. | Pins that only heuristic edges carry a tier and the heuristic count is appended last. |
| `src/resolve/tests/nested_step.rs` | The enclosing-type step of the ladder. | Pins that the innermost enclosing type wins and a dotted ref never enters the step. |
| `src/resolve/tests/ctor_di.rs` | Constructor-injection edges. | Pins sole-implementor binding and ambiguity on a tie. |
| `src/resolve/tests/dispatch.rs` | The type-level and member-level `implements`/`overrides` edges. | Pins that an unresolvable registration type emits no edge, that a repository with no registration facts gains no dispatch edges at all, and that two arity-tied overloads leave a member-level edge unemitted. |
| `src/resolve/tests/receiver_hops.rs` | Property hops and `var` typed from a call's return. | Pins that an unrecorded property or an ambiguous type stops the hop. |
| `src/resolve/tests/base_members.rs` | `this`, `base` and inherited field receivers; local, catch and parameter shadowing. | Pins that an in-file local shadows a same-named field fact. |
| `src/resolve/tests/base_walk.rs` | Base-walk order, overload arity admission, chain tails through base methods, static qualifiers on bases. | Pins declaration-order base visiting before interfaces. |
| `src/resolve/tests/lambda_parameters.rs` | Lambda parameters typed from delegate parameters of in-graph callees. | Pins positional typing and the untyped outcome for disagreeing overloads. |
| `src/store.rs` | SQLite freshness stores: `cache.db` per root and the shared content database. | Opened with WAL and a five-second busy timeout; migrations are idempotent and a fresh database takes none of them. Populated only by the hooks; `stats` reads and `clear` prunes. |
| `src/suggest.rs` | Did-you-mean suggestions for a query that matched nothing. | Suggestions are printed and never substituted for the query or run. At most `SUGGESTION_CAP` names, nearest first. |
| `src/telemetry.rs` | The one place `find`/`refs`/`read`/`impact`/`tests` invocations are logged: one JSON line (`ts`, `schema_version`, `verb`, `seed`, `outcome`, `elapsed_ms`, `result_bytes`, `candidate_count`) appended to the query log when `SCOUT_TELEMETRY=1`. | Gated on the literal value `"1"`; every other value, including unset, is off. The log path mirrors `graph/paths.rs`'s `graph_dir`: the git common dir when there is one, `<root>/.scout/log/` otherwise. Every failure -- the switch off, an uncreatable directory, an unwritable file -- is silent; a verb's exit code and output never change. |
| `src/tsgraph.rs` | Resolution of TypeScript-family fragments: relative specifiers, `tsconfig` `paths` / `baseUrl` aliases with the `extends` chain, re-export barrels, the TS edge kinds. | A bare specifier resolves only as far as the repo's own tsconfig takes it; anything else is `external`, never a guess at a package's internals. Barrel hops are bounded and the barrel a source names is kept as `via` on the edge. |
| `src/walk.rs` | Source-tree walking, `SKIP_DIRS`, `SOURCE_EXT` and the default file-purpose heuristic. | `SOURCE_EXT` is the one list of indexed extensions; a file outside it is invisible to every verb. `SKIP_DIRS` matches a single path component exactly, at any depth. Roots handed in are absolute and normalised. |

## Where a change goes

| To add or change | Go to |
| --- | --- |
| A C# construct the index should record | `src/extract/walk.rs` for the walker, `src/extract/members.rs` or `src/extract/type_defs.rs` for the member/def facts it carries, `src/graph/fragment_types.rs` for the fragment shape and `src/graph/paths.rs` for its cache version, a fixture under `fixtures/` with its README, the construct's row in `docs/csharp-coverage.md`. |
| A resolution rule or tier for `uses-member` | `src/resolve/ladder.rs` for a type-resolution step, `src/resolve/receiver.rs` for receiver typing, `src/resolve/assembly.rs` for a heuristic tier, with the test file of the same topic under `src/resolve/tests/`; if the edge gains a tag, `src/graph/edge.rs` (`Edge`) and `src/graph/artifact.rs` (schema version), and `src/audit.rs` (tier scoring); then re-run the semantic audit on the fixture and record the numbers in `docs/benchmarks/`. |
| A TypeScript edge kind or alias rule | `src/extract/ts_fragment.rs` (and `src/extract/ts_purpose.rs` for a purpose-only change) and `src/tsgraph.rs`; fixture under `fixtures/ts-grammar/`; the Limitations list in `README.md`. |
| A query verb | A model and builder in the matching `src/query/*.rs` (`find.rs`, `refs.rs`/`refs_tables.rs`, `read.rs`, `impact.rs`, `coverage.rs` for tests-reaching-a-symbol), the matching test topic under `src/query/tests/`, text and compact renderers in the matching `src/render/*.rs`, JSON rendering plus argument parsing and usage text in the matching `src/cli/*.rs` file, a `src/telemetry.rs` `record` call at the verb's own resolved-answer arm in that same file, the command table in `README.md`, an integration test under `tests/`. |
| A CLI flag | The verb's argument loop and usage string in its own `src/cli/*.rs` file (`refs.rs`, `read.rs`, `impact.rs`, `coverage.rs`, `find.rs`), the matching option on the query model, `README.md`. |
| A fixture | A directory under `fixtures/` with a README stating what it pins, plus the integration test under `tests/` that drives the binary against it; fixture vocabulary is invented, never copied from a real codebase. |
| A DI-registration or dispatch edge | `src/extract/refs.rs` for the registration shape rule and `src/graph/fragment_types.rs` for `Fragment.registrations`/`FragDef.override_methods`, with `src/graph/paths.rs` for the fragment cache generation; `src/resolve/dispatch.rs` for resolution and `src/graph/edge.rs`/`src/graph/artifact.rs` for the edge kind and schema version; `src/query/index.rs` and `src/query/dispatch.rs` for the adjacency wiring and `IndexOptions.include_dispatch`, `src/query/refs.rs`/`refs_tables.rs`/`impact.rs`/`coverage.rs` for the verbs that read it; `src/render/refs.rs`, `src/query/json.rs` and `src/cli/args.rs` for text/compact/JSON rendering and `--no-dispatch`; a fixture under `fixtures/` with its README, an integration test under `tests/`. |
| A markup format | `src/markup.rs` for the facts and `src/walk.rs` for the extension. |
| A hook behaviour | `src/hookio.rs` for the rewrite, `src/store.rs` for what is remembered, `src/initcmd.rs` for how it is installed. |
| A persisted artifact or its location | `src/graph/artifact.rs` (the file shape) or `src/graph/paths.rs` (its location), or `src/manifest.rs` for the manifest, `src/repo.rs` for where roots and the git common dir come from, the storage table in `README.md`. |
| An environment variable | Read it where it applies, list it in the README environment table. |
| A module | Declare it in `src/lib.rs` with a one-line doc, add its row above, keep it clean under `cargo clippy --all-targets -- -D warnings` as `CONTRIBUTING.md` asks. |

## Tests

Unit tests live beside the code they cover; integration tests under `tests/` drive the built
binary against `fixtures/`. `tests/conformance.rs` runs the public conformance suite;
`tests/semantic_audit.rs` and `tests/semantic_audit_direction.rs` score the resolver against the
committed oracle snapshots that CI also regenerates. Nothing in the suite reaches the network.