devscout-rs 0.9.0

Fast code index for C# and TypeScript codebases -- map, find, refs, impact from the CLI.
# 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
                           │   rsgraph (Rust)┘                             │
                           └───────────────► 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, `rsgraph` appends Rust definitions and the edges its module tree and
  `use` scope resolve after the resolve,
  `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 (including `rsgraph`) 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` | Root of `audit --semantic`: the module header (join rule, split rationale), the `mod` list and the public re-export. | Holds no code. The public surface is `cmd_audit`; every other item is `pub` at most within `audit`'s own module tree. |
| `src/audit/model.rs` | The shared row/`Inputs` shapes: `Tier`, `EdgeRow`, `DefRow`, `OracleRef`, `Unit`, `Inputs`, `AuditOptions`. | No I/O and no scoring logic; every field is `pub` within the crate so the sibling modules below can read it directly. |
| `src/audit/load.rs` | Filesystem reads: `load`, `parse_graph`, `read_jsonl`, `tier_of`, `build_universe`; the lane comes from `lane.rs`. | Returns a plain `String` error, the shape every `cmd_*` in `cli.rs` wraps as `"error: {msg}"`. `graph.json` is read as a bare `serde_json::Value`, never through `graph::read_graph`. |
| `src/audit/lane.rs` | The lane `audit` reports: `AuditLane`, its `read` from graph.json's `stats.lane`/`stats.semantic`, the text header token and the `lane_detail`/`compilations` JSON keys. | `enriched-partial` when `stats.lane.state` is `partial`, else `enriched` when `stats.semantic` is present, else `syntax`. A graph without `stats.lane` reads, renders and gains no key exactly as before; the header names the partial or not-applied reason only when the record carries one. |
| `src/audit/scoring.rs` | Pure matching/structural-check primitives: `target_matches`, `member_matches`, `target_known`, `short_name`, `file_to_unit`, `reach`, `is_structural`. | No I/O. `target_matches`/`target_known` both apply the enum-member two-spelling allowance identically. |
| `src/audit/report.rs` | `TierStats`, the per-tier counters `score.rs` fills and `render.rs` reads. | `structural` counts false positives only, never a structurally-impossible true positive. |
| `src/audit/score.rs` | `AuditReport` and `score`: the pure `Inputs -> AuditReport` scoring pass, including the `--fp-sites` row collection when `Inputs.collect_fp_sites` is set. | Pure: every branch reads only `Inputs` and locally built indexes over it. 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/audit/render.rs` | Text (default) and `--json` rendering: `render_text`, `render_json`, `ratio_text`, `ratio_j`. | A zero-edge tier row is omitted, never printed as zero. |
| `src/audit/assert_check.rs` | `--assert <file>` threshold evaluation: `evaluate_assert`, `lookup_metric`, `fmt_num`. | Collects every violated or missing key before returning, never stops at the first. |
| `src/audit/fp_sites.rs` | One row per false positive written by `audit --fp-sites <file>`, built inside `score.rs`'s own scoring loop from the same evidence set that decided the edge's class. | Built only when `Inputs.collect_fp_sites` is set; the row count of one class and tier always equals exactly that tier's counter for the class, by construction. |
| `src/audit/cmd.rs` | `cmd_audit`, the CLI entry point `cli.rs` dispatches `audit` to. | Reads the graph read-only. `--json` keeps `--assert` violations on stderr so stdout stays one parseable JSON object; text mode appends them to stdout. With `--json` anywhere in the arguments every error also goes to stderr and the returned output is empty, which the `audit` dispatch arm does not print. `--fp-sites <file>` writes `fp_sites.rs`'s JSON-lines rendering independent of `--json`/`--assert`. |
| `src/audit/tests.rs` | Shared fixture builders for the audit tests (`fragments_for`, `graph_value_for`, `oracle_ref`) and the `mod` list for the nested test files. | Builds `graph.json` through the real extractor/resolver, never a hand-written `Value`, so no test bypasses the parse path. |
| `src/audit/tests/scoring.rs` | Matching, leak and structural-impossibility scoring cases. | One case per named scoring rule in `scoring.rs`/`score.rs`; every case builds its graph through `tests.rs`'s real-extractor helper. |
| `src/audit/tests/render.rs` | The three rendering-pin cases (`--assert` pass/fail/missing, JSON key order, text layout) plus the fixture-backed `--assert` tests. | Pins the exact text/JSON shape `render.rs` promises; a key-order or column change must update these tests deliberately. |
| `src/audit/tests/semantic.rs` | The audit lane over hand-written `stats` blocks: the label, the text header token, `lane_detail`/`compilations`, their key position, and `--assert` over `compilations.partial`/`compilations.total`. | A graph without `stats.lane` is pinned to render with no new key, so a report from before the lane record reads the same. |
| `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. | `map` prints its summary line and, when the rebuilt graph carries a `stats.lane` record, the lane line after it. 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, the `--json` `freshness` splice, 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. `finish_query`'s `json` flag gates `append_freshness`, which splices `freshness` (from `freshness::index_freshness_state`) onto the answer as its true last key on `refs`/`read`/`impact`/`tests`; `find` has no `--json` shape and always passes `false`. |
| `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` and `--no-bus` are parsed in `index_options` exactly the way `--no-guess` already is, on all four verbs. |
| `src/cli/compiler_facts.rs` | The `compiler-facts run|import|status` verb group: local one-shot engine acquisition, build/CI-artifact import, and read-only status. | `run` and `import` converge on the identical `graph::admit_and_publish` call -- the one admission path for both producers. `status` never opens `graph.json`, reports `coverage: syntax-only` when no artifact was ever admitted, prints `coverage: complete` only when no unit is incomplete and every counted compilation is complete (otherwise `partial (K of M compilations: <tally>)`, or the `incomplete (...)` wording with the tally appended), and reports occurrence coverage (admitted count, non-confirmed states, an explicit unavailable state) by an independent read-only parse of the already-admitted bytes. `run`'s own wrapper omits `--capabilities` entirely when the caller passed none, so the spawned engine's own default (not a second, stale definition here) decides. |
| `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#, TypeScript-family and Rust 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`, `extract_rs_file`, `RsFileExtraction`, `RsFragment`, `RsItem`, 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`, `PublishRecord` (now also `arg_count` and `enclosing_call: Option<EnclosingCallFact>`), `EnclosingCallFact`. | 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. `PublishRecord.arg_count` is the call's own argument count; `enclosing_call` is set only when the call sits inside a lambda body that is itself an argument of an enclosing invocation. |
| `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/argument_types.rs` | Records a call's ordered argument descriptors from syntax, declared symbols, and deferred member lookups. | A named argument withholds positional comparison for the whole call; by-reference and unrecognized expressions have unknown slots. The callee's own invocation supplies the list. |
| `src/extract/argument_member_types.rs` | Adds method result, property, field, conversion, and variance facts needed for argument comparison. | Stores these in reserved `methodParams` entries while preserving existing member projections. |
| `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/type_arity_facts.rs` | Type-parameter and type-argument counts: a declaration's own type-parameter count, each method overload's count aligned with `raw_method_arities`, and the length of a type-argument list written on an invoked member. | `raw_method_type_arities` walks and skips exactly the declarations `raw_method_arities` does, so position k of both lists is one overload. A written list is counted by its direct commas, so a nested argument's own commas never count; a member written without a list records nothing. |
| `src/extract/type_param_sites.rs` | Where each overload's own type parameters occur in its parameter types (`methodTypeParamSites`), and a delegate's return and type-parameter occurrences (`delegateSignature`). | Walks exactly the declarations and parameters `raw_method_params` walks, so the sites line up with the descriptors overload for overload and parameter for parameter; a site names the top-level type argument it sits in after one `Expression<...>` wrapper, or -1 outside every type argument. |
| `src/extract/bus.rs` | Message-bus publish-site facts (`publish_fact`), the nested base type-argument fact a generic consumer base needs (`nested_base_type_args`), and the array-marked subset of a def's own base names (`array_message_bases`, position 0 of a base's type-argument list an `array_type` node); also the call's own argument count (via `refs::invocation_arg_count`) and `enclosing_call_through_lambda`, which walks UP from the call to an enclosing lambda ancestor and one level further to that lambda's own parent invocation. | `publish_fact` resolves a message from a generic type argument, a constructed type (direct or nested in a lambda argument, `object_creation_expression` or `array_creation_expression` alike via `Construction::Typed`), an implicitly typed array creation whose elements all construct the same type (`Construction::ImplicitArray`, `implicit_array_message`), or a plain identifier's declared type read through the ordinary scope ladder -- appending `[]` when `Fact::is_array` says the identifier's own declared type was an array -- never from a bare name merely co-located on the same source line. A message the call writes keeps its written qualifier (`written_message`: `new Ns.M()` records `Ns.M`, `new Ns.M[n]` records `Ns.M[]`), the same segments the site's own type reference records, rebuilt from the qualifier's own tokens with whitespace, comments and other trivia between them left out on every call shape (type argument, construction, explicit and implicit array), so the resolver's last-dot split always sees `Ns.M`, and reads it through the qualified steps; `type_descriptor` itself, shared with method parameters and consumer bases, still drops it, and an identifier argument's message keeps its receiver fact's bare name. A message the call NAMES itself is recorded whatever the method is called, since which verbs count is settled at resolve; the identifier shape alone stays gated on the shipped verbs, being far too common to record everywhere. A message that is the enclosing method's own type parameter records that method's name instead, which is what marks a forwarding wrapper. `nested_base_type_args` records an entry only for a base carrying a top-level type argument that is itself generic; a flat base contributes nothing new beyond `base_generic_args`, which is what `array_message_bases` exists to recover the array bit for, since `base_generic_args`/`generic_arg_descriptors` look through an `array_type` to its bare element. `enclosing_call_through_lambda` is a plain node-kind walk (`lambda_expression`/`anonymous_method_expression` ancestor, then that lambda's own enclosing `argument`/`argument_list`/`invocation_expression`) -- no new tree-sitter query -- feeding the resolver's two structural refusals (see `resolve/bus.rs`) without validating any receiver's own declared type. |
| `src/extract/bus_vocab.rs` | Facts a repository declares about its own message vocabulary: one-type-argument handler registrations (`handler_registration_fact`) and the single type arguments a type's properties wrap (`property_message_args`). | A registration is recognized on an installation-shaped name carrying exactly one type argument, which is the whole discriminator against the two-argument DI registration. Both rules are deliberately permissive: neither decides anything, and the resolver keeps only a registration whose named type carries a generic base and only a property argument on a type it already holds to be a handler, so a loose match here costs fragment bytes rather than edges. |
| `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`, and `type_fact`, the single declared-fact producer that also records the type node's written path. | 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/delegate_args.rs` | A call argument's own delegate-shaped parameter count: `delegate_argument_param_count`, for a lambda literal (counted from its own parameter list) or a bare identifier naming an in-scope local function, passed as a method group. | A named argument and every other method group -- a type's member, an extension method, an ambiguous or shadowed name -- record nothing rather than a guess; a local function counts only when exactly one same-named declaration is in scope and no nearer lambda, local, or `foreach` variable rebinds the name first. |
| `src/extract/lambda_shape.rs` | A call's per-position lambda-literal shape (`lambdaArgShape`): whether the body can only produce a value, and whether the parameters are implicitly typed. | Value-only is a positive reading: only expression kinds C# can never use as a statement expression, or a block with a `return <expr>;` of its own, count; every other body, including a kind the list does not know, reads as possibly void-compatible. A named argument, a method group and an anonymous method record nothing. |
| `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/rs_fragment.rs` | Rust item extraction: `extract_rs_file` records every item with its path, kind and span, marks `mod x;` declarations and their `path` facts, tuple and unit structs, test functions (`#[test]` or `test` carried by `cfg_attr`, read by `rs_attrs`) and `impl` blocks, and composes the file purpose, off one UTF-16 parse. | Walks only the file root and inline module, `impl`, `trait` and `extern` bodies; never a function body, an initializer or a token tree. Paths are file-local (`module::Owner::item`), a repeated path takes a `-N` suffix in source order, and no path carries generic or trait text. The declaration facts are keyed by the suffixed path. A readable file always gets a fragment, empty when the parse fails. |
| `src/extract/rs_facts_types.rs` | The Rust resolution facts a fragment carries: `RsImpl`, `RsModPath`, `RsUse`, `RsBlock`, `RsSite`, `RsRole`. | Every field is omitted when empty, so a file with none of the facts serializes as items only. A scope is the inline-module path inside the file; a block index points into the fragment's `blocks`, which holds only blocks with a direct `use`. |
| `src/extract/rs_attrs.rs` | A Rust item's outer attributes in source order, each `cfg_attr` standing for the attributes it carries: `is_test` (`#[test]` or a path ending in `::test`, plainly or carried) and `path_fact` (a `mod x;` declaration's first `path` attribute). | The `cfg_attr` predicate is never evaluated. Inside a `cfg_attr` only an exact attribute form counts, and a malformed `cfg_attr` stands for nothing. A `path` is read only from a plain string literal without escapes or a raw literal; any other value, or a `path` carried by `cfg_attr`, is unread. Nested `cfg_attr` lists unwind on an explicit stack. |
| `src/extract/rs_paths.rs` | Rust path nodes as segment lists (`path_segments`, `join`, `type_path_node`). | Walks a path's nested `path` fields in a loop; generic arguments are dropped and a leading `::` is an empty first segment. A qualified path or a metavariable has no segments. |
| `src/extract/rs_macro_bind.rs` | The names a Rust macro invocation's token trees may bind (`tree_names`): identifiers outside a call or path position, items, generic parameters and `use` names declared inside them, and whether they hold an `impl` or trait. `narrow` gives the context the invocation's own calls are read in; possible names from every nested group reach the code around a statement or module-level invocation, and `is_statement` marks the former. | An uppercase name inside angle brackets or after a type-position token is a type, and a name opening a `field:` entry in a struct body is a field, not a binding. A lowercase name may declare an item. Can drop a call, never binds a local to an item. Trees are walked on an explicit stack. Invocation names are consulted alongside inherited scope sets without copying those sets per invocation. |
| `src/extract/rs_macro_scan.rs` | Calls inside Rust macro arguments: `scan` reads an invocation's token trees for `ident (` and `path :: ident (`, turbofish allowed. | A name after `.`, `$` or `fn`, a nested macro's name and anything inside a string literal is no call; hits are `call` sites marked `mac`, shadowed like any other call and by every name the invocation's trees may bind. Nested trees are scanned on an explicit stack. |
| `src/extract/rs_scope.rs` | Extraction-time Rust scope: the names a function body's patterns and statement macros bind (`body_locals`), a block's own items, the items a block's or module's macros may declare (`macro_items`), modules extended by item macros (`macro_scopes`), and an item's generic parameters. | Locals are flow-insensitive over the whole body and a nested function is its own body; an uppercase pattern identifier binds nothing. A module macro may add unlisted names even when its arguments contain no identifier, as with `include!`. Every walk is on an explicit stack. |
| `src/extract/rs_sites.rs` | Rust reference sites: `collect_sites` records every path the edge pass binds, with its role (`call`, `value`, `type`, `lit`, `impl`, `pat`), scope, block and `Self` owner, in source order. | No site for a `use` declaration, a method or field name, a macro name, an attribute, a `macro_rules!` body, a literal, a primitive type or an `ERROR` region. A first segment shadowed by a block item, an item a block's or module's macro may declare, or a generic parameter, or a single-segment value shadowed by a local, gives no site; `Self` alone gives none. |
| `src/extract/rs_uses.rs` | Rust `use` declarations flattened into one leaf per bound name, with their scope and block (`collect_uses`). | Groups, globs, `self` in a group and renames each give a leaf; `as _` gives none. Token trees, macro definitions, attributes and modules inside function bodies are never entered, and every walk is on an explicit stack. |
| `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/rs_fragment_types.rs` | The Rust fragment data model: `RsItem`, `RsFragment`, `RsFileExtraction`. | A Rust fragment is tagged `rs: 1`, first and required on read, and carries neither `ts` nor `usings`, so each fragment shape rejects the other two. The resolution facts follow `items`, each omitted when empty; reference sites are keyed `sites` because the other two shapes require `refs`. `opaqueScopes` names modules whose macro output cannot be listed by glob lookup. `modPaths`, last, lists each `mod x;` declaration a `path` attribute decides, with its literal, or without one when the file cannot be told (a `cfg_attr`-carried or non-literal `path`, or an enclosing inline module that carries a `path`). |
| `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/publish_message_qualifier.rs` | The written qualifier on a publish message, per call shape. | Pins the qualified type argument, construction (direct and inside a lambda), explicit and implicit arrays, nested, generic and `global::`-qualified messages, whitespace and comments between qualifier segments dropped on every call shape, and that bare, identifier, pass-through and consumer-side texts are unchanged. |
| `src/extract/tests/using_namespace_fact.rs` | The declaring namespace a plain using records, for block, nested, file-scoped, compilation-unit and `global` placements. | Pins that a using's namespace equals the one its own block's refs record, and that `extract-dump` prints the key only when it is set. |
| `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, and that a qualified declaration keeps its written path in `receiver_type_qualified`. |
| `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/argument_types_fact.rs` | Static argument fact sources and unknown forms. | Pins source order, literal and declared types, deferred member values, and positional withholding. |
| `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/rs_attrs.rs` | Attribute facts: each `cfg_attr` test form, plain test attributes, the first `path` literal of a module declaration, suffixed declarations. | Pins the negatives (`derive`, `allow`, `test_case`, every malformed `cfg_attr`, a test attribute on a non-function), unread `path` values (escaped, byte string, macro, carried by `cfg_attr`), children of a `path`-carrying inline module at any depth, a file without the forms serializing without `modPaths`, and deep `cfg_attr` and module nesting on a small stack. |
| `src/extract/tests/rs_facts.rs` | The Rust declaration and `use` facts: every `use` form, block-level uses and their blocks, `mod x;` against inline modules, constructors, test attributes, `impl` records. | Pins that `as _`, uses inside macros and function-body modules, a nested test function and a tuple self type are not recorded, and that a file with no facts serializes as items only. |
| `src/extract/tests/rs_fragment.rs` | Rust item extraction: every kind in every walked context, span starts, unrecorded constructs, syntax errors, self-type heads, path suffixes, raw identifiers. | Pins the line rules (doc comments and attributes start the span, a plain comment never does) and that every path is safe as a shell argument. |
| `src/extract/tests/rs_fragment_cost.rs` | Test-only generated item-cost row over frozen Rust sources. | Reports recorded items, modeled ancestry work, parent calls, path bytes and fragment bytes without changing release artifacts. |
| `src/extract/tests/rs_macro_scan.rs` | Calls inside macro arguments: `assert_eq!`, `assert!`, `vec!`, `matches!`, `format!` after the format string, nested invocations, path and turbofish calls. | Pins the misses: `$f(` in `macro_rules!`, `.f(`, names inside strings, a nested macro's name, `fn f(` in a token tree, attribute arguments; shadowing, including by a name the tree may bind and a statement macro's possible item, but not by a type written in the tree; source order; a deep token tree on a small stack. |
| `src/extract/tests/rs_sites.rs` | Rust reference sites: every callee form, every listed type position, literals, impl traits, values, pattern qualifiers, `Self` owners, per-segment lines, scope and block. | Pins every drop (locals, closures, nested functions, generics, method names, skipped regions, qualified paths) and a deeply nested body walked on a small stack. |
| `src/extract/tests/rs_purpose.rs` | The Rust file purpose: inner-doc and plain-comment prefixes, grouped item segments, the shared length cap. | Pins that an outer doc, an attribute or an item first gives no prefix, and that a file with neither prefix nor item gives `None`. |
| `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/method_type_arity_facts.rs` | `methodTypeArgCount`, `methodTypeArities` and an extension entry's `typeParamCount`. | Pins that only the invoked member's own list counts, that per-overload counts stay aligned with the value ranges in every part of a partial type, and that two extension overloads differing only in type-parameter count stay separate entries. |
| `src/extract/tests/type_param_site_facts.rs` | `methodTypeParamSites` and `delegateSignature`. | Pins that a method's own type parameter is told apart from its type's, the site positions for `Func`, `Action`, a seed parameter, an expression-tree wrapper and arrays, per-part alignment in a partial type, and a delegate's return and type-parameter occurrences. |
| `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/lambda_shape_fact.rs` | `lambdaArgShape`: value-only and implicitly typed lambda bodies at each argument position. | Pins the value-only and statement-expression forms, returns owned by a nested function, explicit against implicit parameter typing, and that named arguments, method groups and anonymous methods record nothing. |
| `src/extract/tests/delegate_arg_arity_fact.rs` | The `lambdaArgArity` fact: a lambda literal's and an in-scope local function's own parameter count, recorded per call-argument position. | Pins that a nearer lambda, local, or `foreach` variable of the same name shadows the local function and that a named argument or any other method group records nothing. |
| `src/extract/tests/explicit_implementation_facts.rs` | Explicit interface implementations (`IFace.Member` declarations) against the ordinary-lookup member lists and the class-scope receiver facts. | Pins that an explicit declaration never takes a property name's first-declaration slot while the per-overload tables and `names` keep it. |
| `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/extract/tests/bus_facts.rs` | Message-bus publish-site facts and the nested base type-argument fact. | Pins that a same-line bare name never supplies a publish site's message, that a call split across several source lines is still one site, and that a base whose arguments are all flat records no `baseTypeArgs` entry. |
| `src/freshness.rs` | Query-time index-freshness state for a programmatic consumer: `FreshnessState` (`Fresh`/`Stale`/`Unknown`), `UnknownReason`, `index_freshness_state`. | Calls the exact primitives `manifest::freshness_warning` uses (`read_index_state`, `git_head`, `read_manifest`, `dirty_indexed_files_at`) so the two can never disagree about the underlying facts; distinguishes "no index-state.json" from "git unavailable" from "genuinely fresh", which `freshness_warning`'s stderr line collapses together. Adds no logic to `manifest.rs`, which is frozen at its exact current line count. |
| `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/argument_types.rs` | `FragArgType`, the fragment shape of one written call argument's static type: a known descriptor, or a member whose declared type is read after every fragment is indexed. | Untagged, so a known type is a bare string; rides on `FragRef.argTypes`, whose null slots are unknown. |
| `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 `[]`. `Stats.lane` is the last stats key and is written only when a lane record is due. |
| `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/call_shape_types.rs` | The fragment shapes of the lambda-applicability facts: `FragLambdaShape` on a ref and `FragDelegateSignature` on a delegate def. | Field order is significant and each rides on an omitted-when-absent key of `FragRef`/`FragDef`. |
| `src/graph/compiler_facts.rs` | Root of the compiler-facts artifact: the `mod` list, the public re-exports, the engine locator (`SCOUT_COMPILER_ENGINE`), `admit_and_publish`, `read_compiler_facts`, and `PublishError`. | Holds the one publication function: on `admit` returning `Ok`, atomic-writes the original candidate bytes unchanged via `super::paths::atomic_write_bytes`, never a re-serialized form. A refusal writes nothing. |
| `src/graph/compiler_facts/context_summary.rs` | The derived context-summary fold: `recompute`, pure, no I/O. | Canonicalizes each compilation's `identity` via `serde_json`'s default sorted-key `Value` serialization, so the same logical compilation list folds identically regardless of the producer's own JSON key order; a `null` fingerprint is a fixed sentinel that cannot collide with a real 40-hex-character one. Independently reimplemented, byte-for-byte, on the C# producer side. |
| `src/graph/compiler_facts/compilation_tally.rs` | The counted compilations of an admitted artifact: `CompilationTally` (`from_artifact`, `from_states`), its `K of M` summary and lane detail. Pure, no I/O. | Counts only `complete`, `partial` and `failed`; `unsupported`, `excluded` and a state-less entry are not counted. Reasons of the incomplete ones are listed in ascending byte order, an empty reason as `unspecified`. The coverage line and the `map` lane read this one definition, and admission never calls it. |
| `src/graph/compiler_facts/admit.rs` | The pure admission core: `admit`, `AdmittedFacts`. No I/O. | Fixed check order (malformed encoding, completion record, inventory coherence, occurrence self-consistency, then every identity field) so every refusal test asserts a predictable first-failing-check story. The context-fingerprint identity check recomputes the header summary from the embedded envelope rather than comparing two producer-written copies of one value. A declared-incomplete artifact is admitted, never refused. |
| `src/graph/compiler_facts/artifact.rs` | The candidate header type and its parser: `CandidateHeader`, `Profile`, `Coverage`, `IncompleteUnit`, `CompilationRef`, `parse_header`, and the three `COMPILER_FACTS_*` version constants. | The header is parsed from a `serde_json::Value` view; the bulk payload (diagnostics, symbols, every occurrence field beyond its own `compilation` reference, the rest of the embedded context-envelope body) is never deserialized into an exhaustive struct, so an additive producer field cannot be silently dropped by a re-serialization this module never performs. `CompilationRef` is the one shape admission reads out of a compilation entry: `identity` (opaque) and `fingerprint`. |
| `src/graph/compiler_facts/expectations.rs` | What one admission call checks a candidate against: `AdmissionExpectations`, `expectations_for`, and the `EXPECTED_*` compiled-in constants. | `expectations_for` calls the crate's one existing `git` shell-out (`manifest::git_head`) for the source-snapshot expectation; no new shell-out site is added anywhere in this crate. `EXPECTED_ENGINE_REVISION` names the engine revision that can emit occurrence facts. |
| `src/graph/compiler_facts/paths.rs` | Compiler-facts artifact path resolution and its superseded-generation sweep. | Reuses the already-public `graph_json_path` instead of the private `graph_dir`, so this file needs no `super::paths` access despite living beside `graph.json`. |
| `src/graph/compiler_facts/reasons.rs` | The closed refusal vocabulary: `RefusalReason` and its stable machine-readable token per variant. | `Display` writes the exact token named in each variant's doc comment; nothing here is reworded once shipped, since callers match on it. Three variants are occurrence-specific, broken-run family: a capability/payload disagreement, an occurrence naming an unknown compilation, and one naming an unsupported compilation. |
| `src/graph/compiler_facts/run.rs` | The local-acquisition entry point: `run_engine`, its default timeout/output-cap constants. | Captures stdout under a byte cap and a wall-clock timeout with no shell (`std::process::Command` invoked directly); no code path here writes to the artifact path -- only `admit_and_publish` does. |
| `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. Each `extract::PublishRecord` maps to one `FragPublish` field-by-field, including its `arg_count`/`enclosing_call`; a markup fragment's own `publishes` stays `Vec::new()`, so it needs no `FragPublish` construction at all. |
| `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#, a TypeScript and a Rust fragment; `FragPublish` now also carries `argCount`/`enclosingCall: Option<FragEnclosingCall>`; `FragDef` also carries `arrayMessageBases: Vec<String>`, appended LAST after `propertyMessageArgs`. | `AnyFragment`'s three arms are mutually exclusive by field shape (a TS fragment carries `ts`, a C# one `usings`, a Rust one `rs`, and none carries another's required key), so a fragment cache file can hold all three 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. `FragPublish.argCount` is omitted when zero, `enclosingCall` when absent -- both stay within the fragment generation already current. `FragDef.arrayMessageBases` is omitted when empty; it names the subset of `bases` whose message-position argument at `baseGenericArgs`' own position 0 is an array type, the one bit `baseGenericArgs` itself cannot carry. `FragDef.methodTypeParamSites`/`delegateSignature` and `FragRef.lambdaArgShape` joined with the v27 cache generation and are omitted when empty or absent. |
| `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/lane.rs` | The `stats.lane` record: `LaneStats`, its `applied`/`partial`/`not_applied` constructors and `with_compilations`, `LaneCompilations`, `LaneReason`, and `line`, the text `map` prints. | The persisted fields are strings so a graph carrying a state or reason this build does not know still reads; this build writes only what its constructors produce. `reason`, `detail` and `compilations` are omitted when absent; `compilations` is appended last. |
| `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 (including the project-units and Rust crate-model sidecars), 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`, `rebuild_graph` and `rebuild_graph_with_crates`, which also takes the Cargo package model and mirrors it into its sidecar. | 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. Resolution uses only the lane decision's layer, and its record is written to `stats.lane`. Rust fragments reach neither the C# nor TypeScript resolver; `rsgraph::append_rust` and `append_rust_edges` project them after resolution. |
| `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/cache/qualified_receiver.rs` | The current generation's rename as the invalidation of a cached receiver fact that lacks its written qualified type. | Pins that a prior-generation entry is re-parsed and binds the declared type like a cold run, while the same entry at the current generation is reused and binds the same-named decoy. |
| `src/graph/tests/compiler_facts.rs` | `admit`'s fixed check order and refusal tokens, the declared-incomplete-coverage admit path, `admit_and_publish`'s atomic-write/refusal-leaves-artifact-untouched behaviour, and occurrence-payload admission (the three new refusal classes, lossless round-trip, and freshness pairing). | Pins that a contract-version mismatch is reported before an engine-revision mismatch even when both are wrong, that a refused republish leaves a previously admitted artifact byte-identical with no stray tmp file, and that admission succeeds against a real multi-compilation envelope with no top-level `fingerprint` field -- the shape the pre-delta self-referential check could never have admitted. |
| `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/fragment_types/argument_types.rs` | `FragRef.argTypes` serialization. | Pins that the slots keep source order, an unknown slot is `null`, a member slot omits absent `receiverArgs`, and a ref without the fact omits the key. |
| `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/lane.rs` | `LaneStats` serialization, reason tokens and the text line. | Pins the exact bytes of an applied and a not-applied record, that an unknown state reads back, and each line form, including a detail that itself contains `:`. |
| `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. The language census lives in `initcmd/census.rs`. |
| `src/initcmd/census.rs` | The `init` language census: counts the walked files by extension and groups them by support tier (fully supported, indexed and graphed, indexed and graphed with path-resolved edges, not indexed). | Counts exactly the file set `walk::list_source_files` enumerates, never a second walker. Its own sibling file because `initcmd.rs` sits at its size-ratchet ceiling. |
| `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 and a `Cargo.toml` edit through the Rust crate-model sidecar, not through the fragment index. The per-language reparse dispatch lives in `mapcmd/dispatch.rs`. |
| `src/mapcmd/dispatch.rs` | The per-language half of `map` (C#, markup, TS/JS, Rust): which files are graph sources, the lossy source read, the best-source rule, the per-file reparse, the fold into purposes and fragments, and the `ast-none` fallback. | Its own sibling file because `mapcmd.rs` sits at its size-ratchet ceiling; a new language is added here, and `map_repo` keeps only the language-free orchestration. |
| `src/mapcmd/options.rs` | `MapOptions`: `hash_reuse`, `no_semantic` and `from_env`. | Its own sibling file, not inline in `mapcmd.rs`, because that file sits at its exact size-ratchet ceiling with zero headroom. `no_semantic` is a CLI flag (`map --no-semantic`), never an environment switch; a `--no-semantic` run always rebuilds and records its lane. |
| `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#, the TypeScript-family grammars and Rust (`is_rust`, `parse_rust`); 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/reference/csharp-constructs.md` names the pin its tables were checked against. The plumbing verbs stay C#-only. |
| `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/bus.rs` | `bus-hop` query-layer plumbing: `record_bus_edge` (the index-build-time adjacency push, the one-kind mirror of `dispatch.rs`'s own `record_dispatch_edge`), `BusDirection`/`BusHopRow` (the provenance row `refs`/`read` render), and `symbol_bus_rows` (the row builder for one resolved symbol, inbound then outbound). | `impact`'s own reach needs no code here: `bus_hop` joining `inbound_walk_kinds` (`dispatch.rs`) and `why.rs` naming `Why::BusHop` are enough. `evidence` is a pass-through of `resolve/bus.rs`'s own closed word list, never respelled. |
| `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`), the outbound kind indices and ranking helpers (`RankedOutbound`, `outbound_foreign`, `push_ranked`), `inbound_walk_kinds` (the seam `impact` crosses every inbound kind through, `bus-hop` and the four TS kinds included), and `note_dispatch_files` (the manifest-flag/hub-count bookkeeping `implements`/`overrides`/`bus-hop` all share). | 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` (now also `bus: Table<BusHopRow>`), `TestVia`, `test_bus_rows`. | A file's own attributed test def outranks the project-model vouch, which outranks a TS/TSX/JS test-file name (`test_path.rs`); 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. `test_bus_rows` reuses `query::bus::symbol_bus_rows` (uncapped, then capped after filtering) rather than a second bus-hop lookup, keeping only `In`-direction rows whose publish site is itself a test file (the SAME `test_defs_by_file`/`is_test_file` rule `collect_test_rows` applies); a separate field by construction, so a possible route can never inflate `test_file_count`/`ref_count` or their heuristic twins. |
| `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 PLUS its own forward `bus-hop` half (a file whose declared def contains a publish site also affects every handler that site's message reaches, reading `index.outbound_by_file` the reverse walk never needed), the interface hop and its fan-in brake, the hub brake, `build_impact_model`. Seed resolution lives in `impact_seed.rs`. `KindLines`/`Hit`/`note_line`/`BusOrigin` live in `impact_why.rs`; this file only builds and consumes them. | 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. `--no-bus` needs no special case for the forward half: `outbound_by_file[file].bus_hop` is already empty whenever the suppressor was on at index-build time, the same mechanism the reverse half already relied on. |
| `src/query/impact_seed.rs` | `impact`'s seed resolution: `looks_like_file_path`, `SeedResolution`, `resolve_impact_seed`. Split out of `impact.rs` to keep that file under its size ceiling; a pure move, re-exported through `query.rs` at the same paths. | The type ladder runs first; a member seed, tried only once it has missed, answers as its unique declaring type. A dotted member seed looks like a file path, so it is tried on a file-path miss instead. `next.rs` calls `resolve_impact_seed` to check an `impact` suggestion resolves the same way the verb would. |
| `src/query/impact_why.rs` | `KindLines`, `Hit` (the walk's own per-file bookkeeping) and `BusOrigin` (one `bus-hop` edge's own identity -- `(file, line, message, to, to_file)`, ordered so "the lowest by publisher file/line/message/handler" is one comparison) all live here now, plus an `ImpactRow`'s `why`: `Hit`'s `note_direct`/`note_direct_amb`/`note_iface`/`note_heuristic`/`note_bus` methods, each pairing a `KindLines` slot with the edge that set it (and, for `note_bus`, the edge's own `BusOrigin`), `note_kind_edge`'s bus/direct dispatch, `note_taint`'s possible-route bookkeeping (now carrying the inherited `BusOrigin` forward instead of a bare flag), `primary_why`/`build_visited_entry`, which fold that pairing into one value per file, and `from_lines_of`. 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, over a `bus-hop`, weakest of all. `note_taint` clears `bus_only` the moment any edge to a file traces back to the seed without crossing a bus hop; a file that inherits taint from every one of its own reaching defs stays possible-route-only across hops, carrying the ORIGINATING hop's own identity forward so a downstream row can disclose the hop its marker came from, not just that one exists. A file reached by both a bus-only path and a real path keeps its stronger `why` and drops the origin identity entirely, gated at `build_visited_entry`. |
| `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`. | 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. The four TS kinds join through `ts_edges::record_ts_edge`, by kind rather than language, ungated by any of the three flags. `bus-hop` joins the same way, keyed by its own handler (`to`) inbound and publish-site file outbound, gated by `IndexOptions.include_bus` (`--no-bus`) on the same terms. |
| `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 except `tests` (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`), moved out of `cli.rs` to keep that file under its size ratchet; `j_table`/`push_heuristic`/`j_bus_row` are `pub(super)` so `json/tests_model.rs` and `json/next.rs` reuse them rather than respelling the same shapes. | `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; `bus-hop` (after `memberRefs`, before `outcome`) follows it too. An inbound/outbound row's `occurrenceIndex` is appended after `why`, the row's true last key, present only when `occurrence_index` is `Some`. |
| `src/query/json/tests_model.rs` | `tests_model_to_json`, split out of `json.rs` to keep that file under its flat line limit. | `pub(crate)`, re-exported from `json.rs` so every existing caller keeps reading `query::json::tests_model_to_json` unchanged. Reuses `json.rs`'s own `J`/`j_table`/`push_heuristic`/`j_bus_row` rather than a second copy of any of them. The `bus-hop` key (a test file publishing to this handler over a bus hop) is appended after the heuristic pair, before `outcome`, present only when the model's own `bus.total` is non-zero. |
| `src/query/json/next.rs` | The `next` key: `j_next` and the `*_to_json_with_next` builders for `refs`, `read`, `tests`, `impact` (plain and with imports) and the bare-member shape. Split out of `json.rs` to keep that file under its flat line limit. | `next` is inserted immediately before `outcome`, so `freshness` stays last and no existing key moves. It is an array of argv arrays without the binary name. Each member object of a bare-member answer carries its own `next`, and the wrapper carries their ordered union. Ambiguous and not-found shapes never call these builders, so they carry no `next`. |
| `src/query/json/ts_tables.rs` | `ts_table_fields`: the `call`/`jsx-use`/`dispatch`/`import` keys of a `refs`/`read` `inbound` or `outbound` object. | Split out of `json.rs` to keep that file under its flat line limit. A key joins after every older key and only when its table's total is non-zero, so an answer with no TS edge keeps its bytes. |
| `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/next.rs` | The follow-up suggestions every resolved or zero-hit answer ends with: `NextArgv`, `refs_next` (also used by `read`), `tests_next`, `impact_next`, `find_next`, `merge_suggestions`. | Reads only the already-built model, runs no new query, and spells symbols as def ids and files repo-relative. Every candidate is re-resolved with the resolver its target verb uses (`resolve_symbol`, the member fallback, `resolve_impact_seed`); one that does not resolve to exactly one target, or that a verb would read as a flag, is dropped, so every emitted argv runs. |
| `src/query/occurrence.rs` | Query-time occurrence identity: `tag_inbound_rows`/`tag_outbound_rows` assign `InboundRow`/`OutboundRow.occurrence_index`, and the generic `assign_occurrence_indices` core they share. Called from `refs.rs` once per table, immediately after that table's existing ranking/capping step. | Computes no new fact and touches no persisted graph state -- grouping is by every row field except `occurrence_index` itself, in the table's existing stable emission order; a row with no colliding sibling in its own table gets `None`, and a row dropped by an existing cap is never grouped or assigned one. Scoped to one table of one answer against one graph snapshot. |
| `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`), ranked inbound/outbound capping. The enum member-ref rollup lives in `refs_member.rs`. | 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` and the four TS tables share the one inbound cap, but never share a table header: `refs`/`read` print each only when its own total is non-zero, unlike the first three. `RefsModel.bus` (`bus.rs`'s `symbol_bus_rows`) is a separate, always-built table combining inbound and outbound `bus-hop` rows for the resolved symbol. |
| `src/query/refs_member.rs` | The enum member-ref rollup: `MemberRefEntry`, `MemberRefs`, `enum_member_refs`. Split out of `refs.rs` to keep that file under its size ceiling; a pure move, both types re-exported through `query.rs` at the same paths. | Member order is declaration (def-table) order, never by count, so two runs print the same line; at most `MEMBER_NAME_CAP` member rows are kept and the rest are counted as dropped. |
| `src/query/refs_outbound.rs` | `build_outbound_tables`: the `refs --out` tables of one resolved symbol, every outbound kind ranked under one shared cap. | Split out of `refs.rs` to keep that file under its flat line limit. The ranking is the inbound one mirrored: resolved before heuristic, own project before foreign, then file and line. |
| `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, and now covers `bus-hop` too, which `bus.rs`'s own row builder sorts by. `InboundRow`/`OutboundRow` carry `occurrence_index`, set only by `occurrence.rs`, `None` everywhere else. |
| `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, then a unique `::` tail of a Rust item's path (with or without its `-N` suffix); two or more candidates at any step is ambiguous, never resolved further. The `::` step is last and no C# or TS id contains `::`, so it moves no earlier answer. |
| `src/query/symbol_refs.rs` | `SymbolRefs` and `symbol_refs`: one resolved symbol's immediate inbound and outbound edge lists, precise, heuristic and ambiguous, read from a built `GraphIndex`. | Split out of `index.rs` to keep that file under its flat line limit. An enum's inbound `uses-member` unions every member's edges; outbound unions every declaring file of a partial type. |
| `src/query/test_path.rs` | The one test-file definition `tests`, `impact` and the `tests` bus filter share: `test_via` (attribute, then project, then name) and `is_test_path`, the TS/TSX/JS naming rule. | The name rule covers `.ts`, `.tsx`, `.js`, `.mjs` and `.cjs` -- `*.spec.*`/`*.test.*` in one of them, or a directory segment exactly `__tests__` -- and is case-sensitive; `.jsx` and the `.mts`/`.cts` suffixes are read but never classified, and a C# file can never match, so no C# answer moves. |
| `src/query/ts_edges.rs` | `TsKinds` (one def's or file's `call`/`jsx-use`/`dispatch`/`import` edge lists) and `record_ts_edge`, the index-build-time admission of those four kinds. | Admission is by edge kind, never by the file's language. An `import` edge is keyed inbound under each of its `bound_defs` and is not admitted when it binds none; every admitted edge counts toward the hub in-degree. |
| `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/bus.rs` | `impact`'s possible-route taint: a file reached only through a `bus-hop` edge, at any depth, versus one that also has an independent non-bus path. | Pins that the taint threads through a multi-hop walk and that an independent path clears it, the same way `ctor-di` already outranks a plain reference. |
| `src/query/tests/bus_hub.rs` | The hub in-degree brake reused for `bus-hop` referrers, at its default boundary, and the `--no-bus` suppressor at a case chosen to cross it. | Pins that a bus hop's referring file counts toward the same brake as any other edge kind, one below and one at the threshold. |
| `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/next.rs` | `refs_next`, `tests_next`, `impact_next`, `find_next` and the resolve filter they share. | Pins that a candidate whose target is ambiguous, no longer resolves, names a file the index does not hold, or starts with `--` is dropped while its siblings are kept, and that a member seed and an `also_in` declaring site still get their suggestion. |
| `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 and the Rust `::` path. | Pins the exact-id/unique-name/case-insensitive/dotted-tail/`::`-path order, that two candidates at any step stay ambiguous, and that a `::` query never answers for a C# or TS id. |
| `src/query/tests/test_path.rs` | The TS/TSX/JS test-file name rule and the vouch precedence. | Pins the ten suffixes and the directory segment, the near misses it refuses -- `.jsx` and `.mts`/`.cts` names, case variants, partial segments and a `tests` directory -- and that an attributed def and a test project outrank the name. |
| `src/query/tests/ts_edges.rs` | TS edge admission into the query index: per kind, the unbound-import refusal, the three narrowing flags, the hub in-degree. | Pins that an import binding no definition earns no adjacency and that `--no-dispatch`/`--no-guess`/`--no-bus` leave the TS lists alone. |
| `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. `implements` and `overrides` are appended after the words that pre-date the two edge kinds, so no existing word moves; `bus-hop` follows `imported-edge`, and the TS words `call`, `jsx-use`, `dispatch`, `import` and the `tests` vouch `test-path` are appended after it, the nineteenth and newest. |
| `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. A symbol reached only over a possible-route bus hop (no precise/heuristic row at all) still gets the header and its `bus-hop` block; only a symbol with neither prints the zero-case line. Both renderers reuse `refs.rs`'s own `bus_hop_line` (text) and the same marker-plus-path-to-full-row compact shape, rather than a second copy of either. |
| `src/render/impact.rs` | The `impact` verb's renderers: `render_impact_text`, `render_impact_compact`, and `bus_origin_suffix`. | 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. `bus_origin_suffix` states the SAME disclosure sentence `refs`/`read`'s own bus-hop rows carry (`markers::BUS_HOP_UNVERIFIED`), plus the row's own `bus_origin` identity -- an impact row is a file-level aggregate with no message/handler fields of its own to show otherwise. Compact mode carries a marker (`?`) and a pointer to the full row instead. |
| `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`, `BUS_HOP_UNVERIFIED`. | `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. `BUS_HOP_UNVERIFIED` is the one wording every bus-hop disclosure states, shared by `refs.rs`'s `bus_hop_line` and `impact.rs`'s `bus_origin_suffix` so the two renderers can never drift from each other's wording again. |
| `src/render/next.rs` | The `next:` trailer: `next_line`. | Returns `None` for an empty list, so an answer with nothing to suggest prints no line. An argument outside the shell-safe set is POSIX single-quoted; everything else is printed bare. |
| `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`, the enum member-count line, and `bus_hop_line` (`pub(super)`, reused verbatim by `render/coverage.rs`'s own `tests` bus block). | 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. The `bus-hop` provenance section (publisher, message, handler, evidence, direction) follows the same non-zero-only rule, and its disclosure sentence is `markers::BUS_HOP_UNVERIFIED`, shared with `impact.rs`'s own bus-only suffix. |
| `src/render/ts_tables.rs` | The `call`/`jsx-use`/`dispatch`/`import` blocks of the default and `--compact` `refs`/`read` renderers, and `sums` for the trailer and summary lines. | Split out of `refs.rs` to keep that file's two renderers readable. A block prints only when its total is non-zero; an outbound row names the target definition, since one import statement can bind two names in one file. |
| `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/next.rs` | `next_line`'s empty, single and multi-candidate shapes and its quoting rule. | Pins that no candidates prints no line, that a `::` path or dotted id stays bare, and that an argument a shell would read differently is single-quoted. |
| `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/render/tests/ts_tables.rs` | The TS tables in `render_refs_text`/`render_refs_compact`. | Pins that a TS table prints only when non-empty, after every older kind, and that its drops join the shared trailer and the compact summary. |
| `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. `MemberLists.overload_counts` pairs each part's value ranges with its type-parameter counts position for position before the union, and a name any part cannot pair is listed in `unpaired_overload_names`. Each `MethodOverloadParams` carries its own type-parameter count and sites (`OwnTypeParams`) only when both are recorded and aligned with its descriptors, and the partial-class union keeps two overloads with the same descriptors but different type-parameter facts. |
| `src/resolve/delegate_shape.rs` | `delegate_shape`: the parameter list, `void` return and parameter-type arguments of the delegate a parameter descriptor names -- `Action`/`Func`/`Predicate` structurally, an in-graph delegate through its `Invoke` list and signature fact. | An array answers nothing, one `Expression<...>` wrapper is unwrapped, and an in-graph delegate without a signature fact leaves its return and type-argument roles unknown so each reader fails open. |
| `src/resolve/arity.rs` | Arity admission for instance overloads and extension entries, including generic-argument unification and a call's written type-argument count (`CallArity`, `method_call_admits`). | 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. A written type-argument count must match the type-parameter count of the same overload whose range admits the value count; an unknown count on either side keeps the value-only rule. |
| `src/resolve/argument_types.rs` | Resolves deferred argument members from indexed declarations and a fixed set of common library members. | Unresolved owners or conflicting return types become unknown; receiver type parameters substitute only when their binding is certain. |
| `src/resolve/argument_compatibility.rs` | Pairs arity, lambda and argument-type checks on one overload and vetoes definite type mismatches. | Unknown positions admit; a candidate with any applicable overload admits. Proven inheritance, numeric conversion and possible user conversion keep an overload eligible. |
| `src/resolve/tests/argument_compatibility.rs` | End-to-end must-admit cases for conversion and uncertain argument shapes. | Pins inheritance, numeric, nullable, null, object, dynamic, params, and variance admission. |
| `src/resolve/lambda_arity.rs` | `lambda_arity_admits`: checks a ref's recorded lambda facts against each overload of a candidate: the delegate's parameter count, a void return refused for a value-only body, and a generic overload refused when an own type parameter occurs only in parameter types of delegates filled by implicitly typed lambdas. | One overload must pass at every position, as C# applicability holds per overload. Fails open where there is nothing to judge -- no recorded fact, or no overload named `member`; the return check admits an unreadable return, and the inference check applies only without a written type-argument list and admits on missing facts, an explicitly typed lambda, any other occurrence of the type parameter, or an unreadable delegate. A zero-parameter `Action`/`Func<TResult>` shape still admits nothing at a position with an arity fact. |
| `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. For a read through an interface receiver, a declaring base that is itself a base of another declaring base holding the member as a property or field is hidden and skipped; the first survivor in walk order answers, and a call keeps the first declaring base. 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`: usings as `FileUsings`, 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. `FileUsings` groups each plain using by the namespace that declares it, with compilation-unit and in-scope `global using` directives at `""`, for the ladder's bare-name walk; every other reader sees the flat set it dereferences to. `FileUsings::reaches` answers, per def, whether the file's project can name it (set by `reach.rs`; always true with no model or for a file no project owns). |
| `src/resolve/reach.rs` | `attach_reach`: gives each project-owned file's context the per-def answer to "can this file's project name it", for the ladder's namespace-level walk. | Reachability is the transitive `ProjectReference` closure alone, with no test-project direction. A def is inside when any declaring file (partial parts and duplicate declarations included) is owned by a reachable project, or when some declaring file has no owning project. No model, or a file no project owns, gets no reach and sees every def. One table per project, shared by its files. |
| `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. Steps 2 and 3 run one namespace level at a time, innermost first and ending with the global namespace: the level's own type, then the usings declared at exactly that level, so an enclosing namespace's type beats a using declared further out and a using declared in a block that does not enclose the site never applies. With a project model, the walk passes over a level's type or a using-imported candidate the site file's project cannot reference (`FileUsings::reaches`), after each directive's own first-match reading; steps 0 to 1.5 and 4 do not consult it, and `narrow_by_reachability` still narrows the ambiguities that reach the three consumers. |
| `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`/`bus_vocab.rs` resolve a registration's or a bus-hop message's own type text through, now carrying the reference site's own enclosing-type chain (`outer_types`) so the ordinary ladder's nested-shadow step runs for it exactly as it would for a plain `uses-type` reference at that position; `dispatch.rs`'s own registration-target lookup passes `&[]` (unchanged, not itself a reference site with a stack of its own). |
| `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/bus.rs` | Message-bus `bus-hop` edges: `kept_publish_sites` (every publish site the pass keeps, resolved to a `MessageIdentity` and filtered ONCE, ahead of vocabulary derivation), `append_bus_edges`, the consumer/handler table it builds from raw `FragDef` facts against the derived vocabulary (full or binding-only role), the closed `evidence` word list, and the two structural refusals (`structural_non_dispatch`, `is_test_double_lambda`) plus the closed public `TEST_DOUBLE_DIRECT_VERBS` seed (Moq's own `Setup*`/`Verify*` surface). | A hop is emitted only when the publish site's own message and the consumer's own base-argument message resolve, independently, to the SAME `bus_vocab::MessageIdentity` (element def plus array rank) through the ordinary ladder in each site's own file context -- never a bare-name or same-line match, so two same-named types in different namespaces, or a message and an array of it, never link. `kept_publish_sites`' own resolved identities are what the vocabulary's admission rule (`bus_vocab::derive_consumer_bases`) reads as "what this corpus sends," so emission and admission never read two independently-filtered publish lists that could disagree. A binding-only base recognizes a def for its property scan alone; its own base-list argument earns no `RawConsumer`. The batch wrapper's inner type argument is read structurally off `base_type_args`' own rendered descriptor, never by naming the wrapper; a base's own FLAT (position-0) argument instead reads `FragDef::array_message_bases` for its array bit, since `base_generic_args` alone cannot carry it. A repository with no publish site, or no consumer, adds nothing. `resolved_consumers` keeps only the FIRST `ResolvedConsumer` per `(def_idx, MessageIdentity)` -- base-list evidence structurally precedes property evidence in iteration order, and an earlier-declared base precedes a later one, so at most one edge exists per route with no separate precedence table. `structural_non_dispatch` refuses a recognized-verb call only when the repository declares EVERY arity-matching overload of that name to take the message's own resolved identity as an input, resolved in the CALL SITE's own context; `is_test_double_lambda` refuses a call inside a lambda sitting in an `Expression<>`-typed parameter of a repository-declared helper, or inside a directly Moq-shaped setup/verify call the repository does not itself declare, reading each candidate overload's own required/total argument-count range paired with its parameter descriptors (`verb_overloads`, built once off the raw `FragDef::method_arities`/`method_params` entries of the defs `DefIndex::member_name_to_defs` lists for a verb name -- a public method, property, field or extension method of that name, so a non-public method never stands in for a library call -- paired only WITHIN one def record, since `DefIndex`'s merged `MemberLists::method_params` dedupes each list separately and cannot re-pair them) and trying both the direct reading (the call's own argument count in range, lambda at its own position) and, when the overload's first parameter is `this`-marked, the extension reading (argument count plus one in range, lambda one position over, the receiver implicit) -- a reading that would land the lambda on the `this`-marked receiver itself is never admissible -- so a helper's own extension-form and static-form calls, with an optional trailing parameter passed or left out, all align correctly with no signal beyond each overload's own declared shape; refusal holds only when some overload aligns and every admissible reading lands on an `Expression<>` parameter, an overload with no paired range read as exact-arity -- neither refusal validates a receiver's own declared type, both stay structural. |
| `src/resolve/bus_vocab.rs` | The message-handling vocabulary a repository declares about itself: `derive_publish_verbs` (roots plus wrapper closure, independent of what is sent); `derive_consumer_bases` and its admission rule (`harvest_registrations`, `def_receives_message`, `def_binds_sent_message`) gated on `sent` -- the resolved `MessageIdentity` set of every kept publish site; the pass-through closure (`close_over_intermediates`, now role-aware); `resolve_type_text` (the ladder entry point a bare-or-dotted type text, never a message identity itself, resolves through, taking the reference site's own `outer_types`); `MessageIdentity` (element def plus array rank) and `resolve_message_identity`, the one entry point every message text -- publish, base argument, property binding -- resolves through, splitting a trailing run of `[]` off first (`split_array_suffix`) so a message and an array of it are never the same identity. | A registered type's own generic base is admitted only when the registered type is not itself a sent message AND actually receives -- through a declared parameter (`def_receives_message`, peeling a wrapped parameter's own generic arguments at any depth via `descriptor_type_texts`, each resolved to a full `MessageIdentity`) or a bound property (`def_binds_sent_message`) -- a message the corpus does send; this is what keeps a library equality base (`IEquatable<T>`) from contaminating the vocabulary, since a self-referential argument can never satisfy both halves at once. The registered type ITSELF is compared as `MessageIdentity::plain` (rank 0): a registration is never on an array. A base admitted only through a bound property is kept binding-only (`binding_only_bases`), never promoted to a full consumer base. Inheritance closure runs to a fixpoint so a base reached through a local pass-through intermediate takes the SAME role the base it passes through to holds; wrapper closure is read against a frozen copy of the seed, so forwarding is followed exactly one hop whatever the corpus iteration order. A project or `ProjectReference` is not a boundary: hops cross them freely within one repository. A registration's own handler lookup (`harvest_registrations`) passes `&[]` for `outer_types` -- its facts carry no enclosing-type stack of their own, unlike a publish site's (`publish.outer_types`) or a handler def's (`receiver::def_outer_types`). `concrete_base_argument`'s flat (position-0) reading appends `[]` when the caller's own `first_arg_is_array` flag (`FragDef::array_message_bases`) says so; its nested-wrapper reading needs no such flag, since `base_type_args`' own rendered descriptor already carries any array suffix it has. |
| `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/property_hop.rs` | Resolves a two-segment receiver through its owner's recorded property type, then checks the requested member on that type. | The property's recorded type-argument count selects a same-named generic sibling; an absent list keeps the receiver helper's existing fallback. The target itself must declare an applicable public member, including the typed-receiver delegate gate. The hop does not walk the target's bases. |
| `src/resolve/provenance.rs` | Records which emission arm answered each `uses-member` edge, only when `SCOUT_EDGE_PROVENANCE` names an output path. | `note` runs strictly AFTER the edge it describes is pushed, so nothing here can reach the answer; with the variable unset it returns before touching anything and the graph stays byte-identical. `flush` writes one JSON object per surviving edge in graph order; an edge no arm claimed carries `"step": null`. |
| `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/argument_binding.rs` | End-to-end member binding with known and unknown call arguments. | Pins extension, sibling-interface, base and generic selection plus fail-open controls. |
| `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/method_type_arity.rs` | A written type-argument count held to each candidate's own overloads, precise and extension tiers alike. | Pins that the count and the value range must hold for the same overload, that the extension tier and the scored tier's receiver rule drop extensions of another count, and that missing or misaligned counts leave the value-only rule. |
| `src/resolve/tests/lambda_applicability.rs` | A lambda argument held to an overload it can convert to: void delegates for value-only bodies, type parameters only an implicitly typed lambda would fix, per-overload pairing, and the property hop. | Pins that a written type-argument list, an explicitly typed lambda, another argument or a delegate return keeps the overload, that an in-graph delegate without a signature fact admits, and that a call with no lambda keeps its binding. |
| `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/qualified_receiver.rs` | A receiver declared with a qualified type, from real C# through extract and fragment conversion. | Pins that the written path binds the declared type over a same-named sibling or decoy, that a missing or doubly matched path earns no precise edge, and that an ambiguous qualified answer is still narrowed by project reachability. |
| `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/explicit_implementations.rs` | `this.` calls, property hops and bare names inside a type that implements an interface member explicitly. | Pins that only an interface-typed receiver reaches an explicit implementation and that `implements` pairing still counts it. |
| `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/bus.rs` | `bus-hop` edge resolution: message identity matching, fan-out, batch and mediator-request shapes, inherited consumer bases, the negative cases. | Pins that a publish site links only when both its own and the consumer's message resolve to the same def, that a wildcard base argument and a same-named message in a different namespace never link, and that one message with two handlers emits one edge per handler. |
| `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/level_walk_reach.rs` | The namespace-level walk under a project model. | Pins that a level's type or using candidate from an unreferenced project is passed over (with the model-less answer beside it), that unknown ownership, a reachable partial part and a referenced test project keep a def inside, that a base resolved at its declaration uses the declaring project's closure, that a directive whose own reading is passed over is not read further out, and that step 4 can still answer with a passed-over def. |
| `src/resolve/tests/qualified_publish.rs` | A publish whose message is written with a qualifier. | Pins that the hop reaches the consumer the qualifier names (a `global::` spelling and a comment between qualifier segments included), and that the same call written bare binds the enclosing namespace's type and earns no hop to it. |
| `src/resolve/tests/using_levels.rs` | Bare type names against enclosing-namespace types and usings declared at different namespace levels. | Pins the per-level order (a level's type, then that level's usings, compilation-unit and `global using` directives last), the per-level ambiguity, and that a sibling block's using never applies. |
| `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/member_hiding.rs` | Property reads through interface receivers whose base closure holds more than one declaration of the member. | Pins that a redeclaring interface hides the declaration in its own bases at any depth, and that calls, class receivers, method-only redeclarations and unrelated declarations bind as before. |
| `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/semantic.rs` | Root of the resolve-time compiler-fact consumption path: the `mod` list and the public re-exports. | Holds no code. Reached only from `resolve::assembly`'s own per-reference wrapper, the load point `lane::decide` (called by `mapcmd::map_repo`) and `lane::record` (called by `graph::rebuild_graph`); the resolver itself performs no file I/O. |
| `src/semantic/lane.rs` | The map-time lane decision: `LaneDecision`, `decide` (re-exported as `decide_lane`), `record` (as `record_lane`), and the artifact presence check. | A record is due exactly when `--no-semantic` is given or an entry sits at the compiler-facts path, and the same condition forces a rebuild. `--no-semantic` never reads the artifact; any load failure is `artifact-unreadable`; a stale layer, or one with no complete compilation, is dropped before resolution, so it leaves no `stats.semantic`. A layer that reaches resolution is settled by `record` from `compilation_lane.rs`. |
| `src/semantic/compilation_lane.rs` | The compilation half of the lane decision: `before_resolution` (`no-complete-compilation`) and `after_resolution` (`nothing-confirmed`, `partial`, `applied`). Pure. | Precedence: the freshness reasons are decided first and never carry `compilations`; every record this module writes does. `nothing-confirmed` and `no-complete-compilation` drop `stats.semantic`, so their graph's edges equal the no-artifact graph's. |
| `src/semantic/layer.rs` | The admitted artifact's own reader and compatibility join: `SemanticLayer`, `load`, `lookup`, the Roslyn-to-devscout identity translation (`translate_type_id`, `split_qualified_type`). | Reads the admitted artifact's raw bytes directly, never oracle output and never a second admission path. `lookup` checks freshness first; more than one matching site at one compatibility key collapses to ambiguous rather than choosing one. `freshness` exposes the verdict `load` evaluated, and `compilation_tally` the counted compilations of the same bytes. |
| `src/semantic/precedence.rs` | The same-context override decision and its glue into the resolver: `decide`, `apply`, `track_reference`, `finish`, `SemanticDiagnostics`. | `apply` undoes exactly the edges the untouched ladder just pushed for one reference and replaces them with one compiler-vouched edge; a miss or absent layer is a no-op. Never a post-pass over the finished edge vector. |
| `src/semantic/discovered.rs` | Compiler-discovered-site projection: `project`. | Runs once per admitted artifact, after the main reference loop, not once per reference. Its edges carry `SemanticProvenance::SemanticDiscovered`, never pooled with a per-reference override, and never move the syntax-lane recall denominator. |
| `src/semantic/freshness.rs` | Whole-artifact freshness: `Freshness`, `evaluate`, and the stale-token class (`stale_reason`, `is_import_trigger`). | Compares the artifact's own recorded `sourceSnapshot` against the checkout's current git head and working-tree dirty state, then its recorded MSBuild imports against their files; any mismatch invalidates every fact the artifact carries. The import class is decided beside the code that mints its tokens. |
| `src/semantic/uncertainty.rs` | The eight-state uncertainty vocabulary: `Uncertainty`, `from_resolution`, `from_compilation_state`. | Only `Confirmed` participates in the resolver's override; every other state falls through to the syntax ladder unchanged, and an unrecognised resolution string maps to `Candidate`, never `Confirmed`. |
| `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/truth.rs` | Root of the semantic-truth harness: module overview, the `mod` list, no code of its own. | Declared in `src/lib.rs` as `pub mod truth` with a one-line doc, exactly like every other top-level module; no shipped CLI verb reaches it. |
| `src/truth/capability.rs` | The machine-readable capability matrix: `CapabilityAxis`, `CapabilityState`, `MatrixEntry`, `build_matrix`, `matrix_to_json`. | `Passing` is constructible only from an `ExecutedObligation` witness, itself constructible only when at least one obligation actually ran; the registry-only `build_matrix` never produces one. |
| `src/truth/fault_controls.rs` | The registered fault-control battery: nine wrong-system mutations plus `run_battery`'s exhaustiveness meta-check. | Every registered row's mutation is applied to an in-memory copy only, never a tracked fixture byte; an uncaught control fails its own test rather than being silently dropped. |
| `src/truth/freshness.rs` | Freshness and transformation controls: `TransformationPair`, `FreshnessTrigger`, `evaluate_pair`, run in both directions. | A pair's expected verdict (`Stale`/`Preserved`) is declared before it is ever run, never inferred from what the pair happens to produce. |
| `src/truth/identity.rs` | Symbol and occurrence identity: `SymbolIdentity`, `OccurrenceSpan`, and the legacy `CompatibilityJoinKey`. | `CompatibilityJoinKey` derives neither `PartialEq` nor `Hash`, so nothing can reach for it as identity by mistake; `joins` is its one explicit comparison. |
| `src/truth/manifest.rs` | The case-manifest schema, `validate_case`, and `parse_manifest`. | `validate_case` rejects a case missing any required field, naming it, and refuses a case whose expectation provenance names `scout-semantic`, the producer this harness grades. |
| `src/truth/pin.rs` | The pinned foundation-candidate digest: `compute_pin`, `verify_pin`, `Disagreement`. | `verify_pin` has no mutating counterpart; a disagreeing consumer can only report a `Disagreement`, never rewrite the pin. |
| `src/truth/producer_reader.rs` | Readers that turn committed producer bytes into observations: `read_oracle_refs`, `read_oracle_units`, `read_flowtrace_facts`, `native_dispatch_implements_edges`. | `native_dispatch_implements_edges` runs `crate::extract`/`crate::graph`/`crate::resolve` exactly as `devscout map` does -- real resolver output, never an asserted edge. |
| `src/truth/profile.rs` | The compatibility-profile registry: seven dimensions, `Profile`, `REGISTERED_PROFILES`. | No registered profile claims `Passing`/`Failing`; the four demonstrated targets claim at most `SmokeTested`. |
| `src/truth/red_baseline.rs` | The committed truthful red baseline: `NamedMismatch`, `RED_BASELINE`, `evaluate_run`. | `harness_health` and `analyzer_verdict` are reported as two separate fields on one run; an id absent from `RED_BASELINE` is reported as new, never silently absorbed. |
| `src/truth/report.rs` | Case evaluation and the two-lane report: `evaluate_case`, `ratio_or_undefined`, `TruthReport`, `report_to_json`. | `evaluate_case` fails an empty result against a positive obligation and never reports a failed/partial context as complete; the two lanes are never summed. |
| `src/truth/uncertainty.rs` | The eight-state `Uncertainty` and five-state `ContextHealth` vocabulary. | Every non-`Confirmed`/non-`Complete` variant is only constructible with an explicit reason string. |
| `src/rsgraph.rs` | Projection of Rust fragments into the graph: one def and one name row per item, a test function (`#[test]`, or `test` carried by `cfg_attr`) carrying its own name in `testMethods` (`rs_def_id`, `append_rust`), then the edges the module tree and `use` scope resolve (`append_rust_edges`). | Ids are `<file>#<path>` (file-local path, never a crate path). The rows and the edges are appended after every C# and TypeScript row, and the edges are counted in `stats.edges_by_kind` only, so a tree with no `.rs` file serializes exactly as before. |
| `src/rsgraph/bind.rs` | Rust path binding: `Binder::walk` binds a path segment by segment (scope for the first, then the module, type or trait before), resolves every `use` leaf to a fixed point, and indexes each type's `impl` items and each trait's own items. | Scope order: enclosing blocks, the module's items and explicit imports, its globs, then only the package's library name. An unlistable glob stops ordinary lookup, while only the first segment of a `use` path may pass it. One candidate binds, two or more stop the walk; an alias, a generic and a qualified path bind nothing. The leaf worklist requeues only the leaves that consulted a changed module, so a cycle resolves to nothing and nothing recurses. |
| `src/rsgraph/crates.rs` | Cargo packages in the mapped scope: `discover` reads each `Cargo.toml` by a line scan into `RustCrates` (package directory, name, library name, crate roots, including cargo's nested `tests`, `examples` and `benches` `<name>/main.rs` targets), and `sidecar_differs` compares it with `rust-crates.json`. | A root is only a file the walk listed inside the package: an escaping or unwalked `path` adds nothing, and a `[workspace]`-only manifest names no root. The library name exists only when a library root does. No manifest in scope gives `None` and no sidecar. |
| `src/rsgraph/emit.rs` | Rust edges from bound sites: `uses-member` for a call bound to a function, `uses-type` for a type-naming segment in a type position, literal, constructor or qualifier, type-level `implements` for a crate trait, one `ambiguous` edge per site with capped candidates. | No field or kind is added; every Rust edge is precise, and the C# ambiguity and unresolved counters do not count Rust. Edges follow file, site and segment order. |
| `src/rsgraph/edge_tests.rs` | Shared harness for the Rust edge tests: scratch trees, the crate model over them, and `edges_for`, which extracts synthetic sources and returns the Rust edges. | Scratch trees live outside every git work tree. |
| `src/rsgraph/edge_tests/bind.rs` | Binding: same-named functions each keep their calls, two globs give one ambiguous edge with capped candidates, no candidate gives nothing, a long `pub use` chain and deep nesting resolve on a small stack. | The C# ambiguity counter stays zero for a Rust ambiguity. |
| `src/rsgraph/edge_tests/cost.rs` | Test-only generated glob graphs and cost rows for deep, wide, diamond and cyclic imports. | Pins stable-query reuse, ordered probe replay, fixed-point invalidation, namespace and block/module behavior against the registered work bounds. |
| `src/rsgraph/edge_tests/crates.rs` | Cargo discovery: default target files, nested `<name>/main.rs` targets and the library name binding from them, `path` keys, `[lib] name`, the package-name fallback, workspace-only manifests, malformed and CRLF lines, the sidecar truth table and its write and removal by the rebuild. | Each rule has a negative case. |
| `src/rsgraph/edge_tests/emit.rs` | Emission: the exact bytes of each callee form's `uses-member`, every type position, literal, constructor and `impl`, macro-argument calls, the order after existing edges, and the test carriers with `test_def_count`. | Pins the no-edge cases: method calls, locals, closures, nested functions, generics, alias qualifiers, std calls, std traits. |
| `src/rsgraph/edge_tests/macro_scope.rs` | Names bound inside macro token trees: closure, `let`, `match`, guard, `if let`, `while let`, `for`, tuple and field-shorthand patterns and parameters of functions written in item-like macros shadow the calls beside them; a statement macro's bindings and items shadow the calls after it; generics, items, `use` and `impl` inside a tree shadow its paths and `Self`. | An expression macro can contain a nested item macro that declares a module, so its possible lowercase name shadows qualified calls inside the same invocation. A call in a macro with no such binding still gives its edge, and an expression macro's names do not leak out. |
| `src/rsgraph/edge_tests/modtree.rs` | The module tree: the no-manifest roots, orphan roots, `mod x;` from a root, a `mod.rs` and a plain file, inline modules and their out-of-line children, missing and doubled files, the first-position rule. | Each rule is observed through the edges a call across it gives. |
| `src/rsgraph/edge_tests/opaque_globs.rs` | Globs that cannot be listed: a block glob of std, of an enum, of a crate module re-exporting an external item or itself holding such a glob shadows the enclosing module's items; a module-level one shadows the library name for an expression path but not for the first segment of a `use` path. | Direct, qualified, aliased and re-exported lookup inside a macro-extended module cannot fall through to its fallback glob. Explicit listed items and unaffected calls still bind. |
| `src/rsgraph/edge_tests/path_modules.rs` | `#[path]` module declarations: file level beside the default file, `..` into a sibling directory, a non-mod-rs file's directory, inline modules of mod-rs and non-mod-rs files, the reached file's own children, the first attribute winning, a path naming its own file, and names with spaces, non-ASCII text or a leading dash. | Each rule has a decoy default or misread file that gets no edge; absolute, root-leaving, unwalked and directory-like paths, a `cfg_attr` path and a child of a `path`-carrying inline module reach nothing; no edge from a compiled file lands in a bypassed default file. |
| `src/rsgraph/edge_tests/scope.rs` | The `use` scope: groups, nested groups, `self` in a group, globs, renames and `as _`, `super` chains, `crate`, `self`, the library name by root kind, `pub use` chains and cycles, block-level uses, explicit over glob, a leading `::`. | Each form has a negative case; a `use` declaration itself gives no edge. |
| `src/rsgraph/modtree.rs` | The Rust module tree: crate roots from the package model or the no-manifest rule, `mod` declarations joined to files breadth first (by the default rule, or to the file a `#[path]` attribute names), inline modules, orphan roots, and the library each root reaches by name. | The first position to reach a file places it; a missing or doubled module file reaches nothing; only binary, test, example and bench roots reach the library by name. A `#[path]` is joined lexically against the walk's file index only and never falls back to the default file; an unread, absolute, root-leaving or unwalked one reaches nothing, and a file it reaches owns its directory. |
| `src/rsgraph/scope.rs` | Rust name tables: items by namespace per module, `use` leaves per module and block, macro-extended module scopes, and the lookups over them (`explicit`, `in_module`, `through_globs`, `in_blocks`). | The type and value namespaces are kept apart; an unresolved explicit import still shadows a glob; glob chains are walked breadth first with a visited set. An unlistable glob or a macro-extended module is opaque: ordinary lookup cannot fall through to a lower-precedence item, and a qualified lookup inside the known module cannot use its fallback globs. Only an import's first segment may pass an unlistable glob when nothing concrete binds there. |
| `src/tsgraph.rs` | Resolution of TypeScript-family fragments: relative specifiers, `tsconfig` `paths` / `baseUrl` aliases with the `extends` chain, the def table, and the `call`/`jsx-use`/`dispatch` reference resolution over the binding map `tsgraph/bindings.rs` returns. | 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/tsgraph/bindings.rs` | What a TS file's import and re-export statements bind: `resolve_reexports`, the barrel-following export lookup (`Exports::lookup`, capped at `BARREL_HOPS` with a per-lookup visited map), and `file_imports`, which emits a file's `import` edges, fills each edge's `bound_defs`, and returns its local-name binding map. | Split out of `tsgraph.rs` to keep that file under its size ceiling. A barrel import emits the statement edge plus at most one `via` edge per distinct declaring file; a bound definition rides the edge whose `to_file` declares it, so binding never adds an edge. The reference resolution that reads the binding map stays in `tsgraph.rs`. |
| `src/tsgraph/bindings/tests.rs` | Which definitions each import and re-export form binds, and on which edge. | Pins that binding adds no edge and changes no other field, and that every bound definition is declared in its edge's `to_file`. |
| `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. `.rs` is in it unconditionally: no `Cargo.toml` probe, flag or setting gates Rust. `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/reference/csharp-constructs.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/score.rs` (tier scoring); then re-run the semantic audit on the fixture and record the numbers in `docs/benchmarks/`. |
| A Rust construct the index should record | `src/extract/rs_fragment.rs` for the walker, kinds, identity and purpose, `src/extract/rs_fragment_types.rs` plus `src/graph/fragment_types.rs` for the fragment shape and `src/graph/paths.rs` for its cache generation, `src/rsgraph.rs` for the def and name rows, `fixtures/rust-definitions/` with its README, the construct's row in `docs/reference/rust-constructs.md`. |
| A Rust path the edge pass should bind | `src/extract/rs_sites.rs` or `src/extract/rs_macro_scan.rs` for the site, `src/extract/rs_uses.rs` for a `use` form, `src/extract/rs_attrs.rs` for an attribute fact, `src/rsgraph/crates.rs` for a crate root, `src/rsgraph/modtree.rs` for the module tree, `src/rsgraph/scope.rs` and `src/rsgraph/bind.rs` for binding, `src/rsgraph/emit.rs` for the edge, `src/graph/paths.rs` for the cache generation when the fragment shape changes, `fixtures/rust-edges/` or `fixtures/rust-targets/` with its README, and `docs/reference/rust-constructs.md`. |
| 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 limit in `docs/limitations.md` and its detail in `docs/explanation/resolution.md`. |
| A TypeScript reference kind in the query verbs | Admission in `src/query/ts_edges.rs`, its table in `TsTables` (`src/query/refs_tables.rs`), JSON in `src/query/json/ts_tables.rs`, text and compact in `src/render/ts_tables.rs`, its `why` word in `src/query/why.rs`, the walk in `inbound_walk_kinds`, a fixture case under `fixtures/ts-query-edges/` and its test in `tests/cli_ts_query_edges.rs`. |
| 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 `docs/reference/cli.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, `docs/reference/cli.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 semantic-truth case, profile, or fault control | The case manifest and fixture pack under `fixtures/csharp-truth/`, the relevant `src/truth/*.rs` module (`manifest.rs` for the schema, `fault_controls.rs` for a registered mutation, `freshness.rs` for a transformation pair, `profile.rs`/`capability.rs` for a dimension or axis, `producer_reader.rs` for a new committed-bytes reader), the matching `tests/semantic_truth_*.rs` file, `capability-matrix.json`/`red-baseline.json`/`reports/syntax-only.json`/`reports/enriched.json` regenerated and re-committed where the change touches them. |
| 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 `docs/reference/storage.md`. |
| A compiler-fact artifact | `src/graph/compiler_facts/artifact.rs` for the header shape, `admit.rs` for an admission or refusal rule, `expectations.rs` for a compiled-in expectation, `run.rs` for local-acquisition process control, `paths.rs` for its location; `src/cli/compiler_facts.rs` for the verb group; a fixture under `fixtures/` with its README, an integration test under `tests/`, the tables in `docs/reference/storage.md` and `docs/reference/environment.md`. |
| An environment variable | Read it where it applies, list it in `docs/reference/environment.md`. |
| 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.