# ripwire — every flag, generated from the binary
**This file is generated. Do not hand-edit it.** Regenerate with:
```bash
python3 docs/docs_commands_build.py --bin build/ripwire
```
The flag surface below is read from `ripwire --help`, so it cannot disagree with the shipped
binary. `test/docscommandscheck.sh` fails if it ever does — in either direction.
Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-09-07.md`), trimmed to the first few lines and
scrubbed of local paths. It is illustrative, not a golden: run the command yourself for the
current shape.
> ripwire — the "ripgrep of AI context": parse a codebase, rank symbols by Personalized PageRank,
> stream a deterministic minified XML map to stdout. Zero runtime deps. Languages: C++, C, ObjC/ObjC++,
> Metal (MSL, .metal — C++ grammar), CUDA (.cu/.cuh — tree-sitter-cuda, <<<>>> launches are call edges),
> Python, TypeScript, JavaScript, Java, Ruby, PHP (.php/.phtml), Lua, Elixir (.ex/.exs), Bash, Go, Rust, Swift, C#;
> JSON, TOML, YAML (config keys); Markdown (.md/.markdown — headings are section symbols with spans).
> usage: ripwire
[flags] # default = the ranked map of on stdout
## How to read a section
- **Answers** — the question this flag exists to answer.
- **Try it** — a real invocation and the real output it produced.
- **Shaped by** — other flags that change what this one emits.
- **Caveats** — the limits the binary itself states for this flag. They are extracted from its
own help text, so they cannot drift from the code.
Two limits apply to nearly everything here and are not repeated in every section:
1. **Call edges are heuristic and name-based.** Dynamic dispatch, callbacks and macro-generated
call sites produce no edge, so counts on the graph verbs carry `counts_floor="1"`. **Read a 0
as "none found", never as "none exists."**
2. **A symbol's `amb="K"`** means K of its calls hit a name with several definitions and the
resolver split the weight rather than choosing. Read the source when which-target matters.
## Contents
**understand a codebase cold** — [`--top-k`](#top-k-n) · [`--max-tokens`](#max-tokens-n) · [`--token-budget`](#token-budget-n-k-m-g) · [`--help-task`](#help-task-task) · [`--for`](#for-task) · [`--signatures-only`](#signatures-only) · [`--auto-bodies`](#auto-bodies) · [`--no-route`](#no-route) · [`--adaptive`](#adaptive) · [`--no-mention-boost`](#no-mention-boost) · [`--no-doc-mention`](#no-doc-mention) · [`--lego`](#lego-type) · [`--exemplar`](#exemplar-task-kind) · [`--recall`](#recall-task) · [`--tree`](#tree) · [`--html`](#html-file) · [`--color-by`](#color-by-mode) · [`--order`](#order-mode) · [`--no-stable`](#no-stable)
**navigate / answer a question** — [`--around`](#around-sym) · [`--callers`](#callers-sym) · [`--callees`](#callees-sym) · [`--uses`](#uses-sym) · [`--graph-query`](#graph-query-expr) · [`--external-surface`](#external-surface) · [`--path`](#path-src-dst) · [`--connect`](#connect-a-b-c) · [`--impact`](#impact-sym) · [`--verify`](#verify-claim) · [`--mentions`](#mentions-sym) · [`--affected`](#affected-f1-f2-sym) · [`--exercises`](#exercises-testfile) · [`--situ`](#situ-f1-f2) · [`--handoff`](#handoff) · [`--test-gate`](#test-gate-f1-f2) · [`--grep`](#grep-str-regex-pat) · [`--grep-context`](#grep-context-n-grep-before-n-grep-after-n) · [`--and`](#and-str) · [`--not`](#not-str) · [`--grep-scope`](#grep-scope-line-file) · [`--grep-in`](#grep-in-code-any) · [`--handles`](#handles) · [`--match`](#match-query) · [`--pattern`](#pattern-pat) · [`--query`](#query-terms)
**zoom the detail ladder** — [`--detail`](#detail-n) · [`--pack-signatures`](#pack-signatures) · [`--outline`](#outline-a-b) · [`--expand`](#expand-a-b) · [`--compress`](#compress) · [`--pack-top-n`](#pack-top-n-n) · [`--no-redact`](#no-redact)
**assess quality / structure** — [`--metrics`](#metrics) · [`--deps`](#deps) · [`--hotspots`](#hotspots) · [`--clones`](#clones) · [`--readability`](#readability) · [`--nonlocal-state`](#nonlocal-state) · [`--ensemble`](#ensemble) · [`--quality-panel`](#quality-panel-preset) · [`--context-ratio`](#context-ratio) · [`--naming-calibration`](#naming-calibration) · [`--naming-consistency`](#naming-consistency) · [`--naming-locals`](#naming-locals) · [`--comment-coherence`](#comment-coherence) · [`--cochange`](#cochange-file) · [`--cochange-recur`](#cochange-recur-k) · [`--cochange-groups`](#cochange-groups) · [`--since`](#since-rev-date) · [`--arch`](#arch-file) · [`--arch`](#arch-file-baseline) · [`--arch`](#arch-file-baseline-update) · [`--lint`](#lint) · [`--lint-catalog`](#lint-catalog) · [`--lint-rules`](#lint-rules-dir) · [`--lint-select`](#lint-select-prefix) · [`--lint-ignore`](#lint-ignore-prefix) · [`--sarif`](#sarif) · [`--with-profile`](#with-profile-file) · [`--communities`](#communities) · [`--community`](#community-id) · [`--zoom`](#zoom-depth) · [`--report`](#report) · [`--seams`](#seams) · [`--mermaid`](#mermaid) · [`--owners`](#owners-sym) · [`--dead-code`](#dead-code-dir) · [`--quality-baseline`](#quality-baseline) · [`--allow-dirty`](#allow-dirty) · [`--quality-delta`](#quality-delta) · [`--quality-delta`](#quality-delta-rev-a-b) · [`--dmm`](#dmm-rev-a-b) · [`--quality-ack`](#quality-ack-reason) · [`--ack-only`](#ack-only-substr-substr) · [`--scope`](#scope-glob-glob) · [`--edit-check`](#edit-check-sym) · [`--replace-symbol-body`](#replace-symbol-body-target) · [`--insert-before-symbol`](#insert-before-symbol-target) · [`--insert-after-symbol`](#insert-after-symbol-target) · [`--edit-payload`](#edit-payload-file) · [`--edit-target-file`](#edit-target-file-path) · [`--no-post-check`](#no-post-check) · [`--edit-plan`](#edit-plan-file) · [`--dry-run`](#dry-run-apply) · [`--safe-delete`](#safe-delete-sym) · [`--slice`](#slice-sym-var) · [`--slice-flow`](#slice-flow-back-fwd-both) · [`--slice-depth`](#slice-depth-n) · [`--at`](#at-file-line) · [`--pr-context`](#pr-context-baseref) · [`--merge-scout`](#merge-scout-ref-ref) · [`--plan-lanes`](#plan-lanes-n-task-goal) · [`--plan-lanes`](#plan-lanes-brief-file) · [`--stray-content`](#stray-content-substr) · [`--plan`](#plan) · [`--abi`](#abi) · [`--whereis`](#whereis-sym) · [`--flags`](#flags-substr) · [`--flip`](#flip-name) · [`--layout`](#layout-struct) · [`--field-affinity`](#field-affinity-struct) · [`--doc-drift`](#doc-drift-substr) · [`--doc-drift`](#doc-drift-gateability) · [`--with-history`](#with-history) · [`--plan-lint`](#plan-lint-file) · [`--from-trace`](#from-trace-file) · [`--run-trace`](#run-trace-cmd) · [`--run-timeout`](#run-timeout-seconds) · [`--note-add`](#note-add-target-text) · [`--notes`](#notes) · [`--pack-task`](#pack-task-task) · [`--partition`](#partition-n) · [`--with-graph`](#with-graph) · [`--export`](#export-cc-json-file) · [`--batch`](#batch-file)
**self-diagnosis** — [`--doctor`](#doctor) · [`--agent`](#agent-codex-claude) · [`--skipped`](#skipped)
**security — scan skill files for injection / exfiltration patterns (exit 2 = CRITICAL, 1 = WARN,** — [`--scan-skill`](#scan-skill-file) · [`--scan-skills`](#scan-skills-dir) · [`--force`](#force)
**knobs / modes** — [`--rank-by`](#rank-by-pagerank-authority-hub-rrf-churn-churn-decay) · [`--format`](#format-xml-columnar-rows) · [`--format`](#format-candidates) · [`--legend`](#legend-full-compact) · [`--json`](#json) · [`--limit`](#limit-n-offset-m) · [`--exclude`](#exclude-substr) · [`--map-diff`](#map-diff) · [`--cache`](#cache-path) · [`--index-out`](#index-out-base) · [`--no-cache`](#no-cache) · [`--no-ignore`](#no-ignore) · [`--max-file-size`](#max-file-size-n-k-m-g) · [`--refetch`](#refetch) · [`--scip`](#scip-index-scip) · [`--pin-census`](#pin-census-file) · [`--mcp`](#mcp) · [`--listen`](#listen-host-port) · [`--mcp-token`](#mcp-token-t) · [`--allow-remote-edits`](#allow-remote-edits) · [`--eval-stray`](#eval-stray-file) · [`--eval`](#eval) · [`--eval-retrieval`](#eval-retrieval) · [`--eval-mined`](#eval-mined-file) · [`--eval-skills`](#eval-skills-file) · [`-h`](#h-help) · [`-v`](#v-version)
---
## understand a codebase cold
### `--top-k=N`
**Answers:** keep the N highest-ranked symbols (default 200) — applies to the default map, plain --query, and --format=candidates (incl.
with --for). --for's OWN signature/lego/compose bundle self-limits via --pack-top-n instead — --top-k is INERT there (documented, not fixed — a real fix is a behavior change). --pack-task/--from-trace/--run-trace/--situ self-budget via --token-budget, not --top-k. --top-k=0 emits NO ranked map at all — ONLY the payload you asked for (--expand/--outline/--pack-signatures/--pack-top-n). Use it when you want the body and not the ~200-symbol map that otherwise rides along with it; the root then carries est_tokens= (the payload's price, the number --token-budget gates on), since no map header is there to carry it.
**Try it**
_Same map, capped to the 5 highest-ranked symbols._
```
$ ./build/ripwire . --top-k=5
... [6 more line(s); run it to see the whole thing]
```
**Shaped by:** `--token-budget`, `--recall`, `--graph-query`, `--pack-signatures`, `--expand`, `--from-trace`, `--run-trace`, `--format`
**Caveats (stated by the binary):**
- --for's OWN signature/lego/compose bundle self-limits via --pack-top-n instead — --top-k is INERT there (documented, not fixed — a real fix is a behavior change).
### `--max-tokens=N`
**Answers:** budget the map to ~N tokens (binary-search top-K) — SHAPES the map to fit.
THE FIT IS A BYTE CEILING, and it is deliberately CONSERVATIVE: N is converted at 2.36 B/tok (the densest calibrated language, so N holds for any corpus) times a 0.90 headroom factor. The map's own est_tokens uses THIS corpus's language-weighted rate instead, so a conformant fit REPORTS a number below the N you asked for — expect ~10-20% of N unused. The shaped map discloses both: max_tokens=N (asked) and fit_bytes=B (honoured). Consequence for composing it with --token-budget=N below: the two Ns are different units, so the same N on both is NOT a tautology. At a SMALL N the map's fixed floor (envelope + legend) can exceed fit_bytes with even one symbol emitted — that map says over_ceiling=1 rather than overshoot in silence, and its est_tokens can then exceed N. XML only: the --json map carries no max_tokens=/fit_bytes= keys yet, and its fit is measured in XML bytes. On --recall it SHAPES the doc bundle, and the ceiling is SPLIT ACROSS the docs rather than handed to the top hit: the budget serves the longest rank PREFIX it can give each doc a readable slice, then divides the bytes equally — a doc needing LESS than its share takes only what it needs and the surplus flows to the ones needing more. One long top hit no longer erases the rest of the corpus, and a bigger ceiling never returns FEWER docs. Docs past the prefix are dropped from the BOTTOM of the ranking; selection ORDER never changes. Every cut is DISCLOSED (header total=/shown=/capped=/truncated=/share_bytes=, a per-doc [truncated: X of Y bytes] marker, and a closing (capped: …) note); share_bytes= is that per-doc ceiling and is ABSENT when it bound no doc.
**Try it**
_SHAPE the map to fit ~1500 tokens (binary-search top-K)._
```
$ ./build/ripwire . --max-tokens=1500
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--token-budget`, `--recall`, `--detail`, `--pr-context`, `--from-trace`, `--run-trace`, `--limit`
**Caveats (stated by the binary):**
- THE FIT IS A BYTE CEILING, and it is deliberately CONSERVATIVE: N is converted at 2.36 B/tok (the densest calibrated language, so N holds for any corpus) times a 0.90 headroom factor.
- Consequence for composing it with --token-budget=N below: the two Ns are different units, so the same N on both is NOT a tautology.
- At a SMALL N the map's fixed floor (envelope + legend) can exceed fit_bytes with even one symbol emitted — that map says over_ceiling=1 rather than overshoot in silence, and its est_tokens can then exceed N.
### `--token-budget=N[K|M|G]`
**Answers:** two personalities depending on the verb: - default map / --query / --recall: a CI GATE — exit 3 if the emitted DOCUMENT's est_tokens exceeds N.
That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone. (test/tokenbudgetcheck.sh reports the live MAPE vs tiktoken o200k when tiktoken is installed; the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged. ASSERTS and fails, vs --max-tokens which shapes to fit — composable: set neither, either, or both (e.g. --max-tokens=16000 --token-budget=16K), but see --max-tokens above: the two Ns are measured in different units. Over budget, nothing of the artifact reaches stdout — only a small record naming withheld_est_tokens= vs budget=, the same vocabulary --recall uses, since est_tokens= is normatively about what a run PRINTED. On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected. --json GATES AT A DIFFERENT NUMBER for the same request, and NOT by a fixed factor: the flag measures the DOCUMENT that was emitted, and whether JSON or XML is smaller flips with RESULT SIZE on this corpus. Small: JSON wins (MEASURED on src --top-k=20: est_tokens 1146 XML vs 899 JSON, ~22% smaller). Large: XML wins instead (MEASURED on src --top-k=200: est_tokens 9405 XML vs 9724 JSON, ~3% LARGER) — the crossover sits near top-k~100-150 here, so the same N can pass or fail differently under --json depending on dialect AND size — never assume one direction, measure the request you actually gate. - --for / --pack-task / --from-trace / --run-trace: SHAPES instead of gating — overrides that lens's own default payload budget and trims to fit, always exit 0. --for's header reports est_tokens="N" so its fit is checkable; --pack-task/--from-trace report their budget ledger in the header report line instead. On --for's auto bundle the ceiling is SPLIT, not handed to the signatures first: the sig side's claim caps at the default sig budget and the rest flows to the inline bodies, so a wider ceiling never serves fewer of them (see --for below). Its VERBATIM task echo is bytes no trim can shrink, so past some task length the header floor alone exceeds the ceiling: the lens drops the comment's DUPLICATE echo first (task_echo: dropped (ceiling); task= keeps the verbatim copy), then labels it over_ceiling (--recall: over_ceiling=1) — never a trim it did not actually do.
**Try it**
_GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape)._
```
$ ./build/ripwire . --token-budget=100
```
**Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--recall`, `--handoff`, `--pr-context`, `--from-trace`, `--run-trace`
**Caveats (stated by the binary):**
- That is the map PLUS every block appended after it (///), each charged from the bytes it actually emits at the calibrated rate for what those bytes are — so --pack-top-n=3 --token-budget=600 gates on the ~67KB it would stream, not on the map alone.
- the estimate is calibrated, never exact — Claude's tokenizer is not public.) Within budget: exit 0, output unchanged.
- On --recall the check likewise runs BEFORE a byte of the bundle is emitted: stdout gets the header line naming what was withheld, never the artifact just rejected.
### `--help-task=TASK`
**Answers:** deterministic enhanced help: recommend ONE executable Ripwire CLI command for this repository and task, or abstain when evidence/applicability is insufficient.
Reports the intent, integer score/margin and repository facts; never calls a model, executes the recommendation, or accesses the network. Structured claims/traces/symbols outrank lexical cues. Recommendation only; pipe trace text to stdin for --from-trace=-.
**Try it**
_The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing._
```
$ ./build/ripwire . --help-task="write a cheerful release announcement"
```
**Caveats (stated by the binary):**
- never calls a model, executes the recommendation, or accesses the network.
### `--for=TASK`
**Answers:** the task lens: ranked signatures + metrics framed for reuse.
The bundle enforces a ~7.5KB default payload budget (tail entries trim first; marks it: T rows handed to the trim, S printed). The r=1 (top-ranked) row carries next="--expand=FILE:NAME" — the one pasteable follow-up, the body that ends the search (defined here, not in the bundle's own header, which the token ladder does not charge). An explicit --token-budget=N overrides the default at the conservative byte rate (SHAPES, exit 0; see --token-budget above) and the header reports the delivered est_tokens. TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6). The root discloses it: bundle="auto" bodies="N" (bodies="0" reason="budget" when none fit) — on EVERY auto-mode run: a ceiling the signatures alone exhaust still carries the attribute (legend and empty shell dropped there; only the attribute has reserved bytes), and --for --json, which serves no bodies by design, says so with "bundle":"sigs". Only the caller-chosen postures (--signatures-only, --detail=N) are attribute-free. ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it. If the anchor's own body does not fit, the bundle serves nothing and says so, and the per-item over-budget comment names what was dropped. COMPACT ON THE CONCEPTUAL ROUTE: a query that anchors nothing (subtoken+body) gets the ranked map plus a section — the same candidate head's ONE-HOP callee signatures, the block a body carries — and NO body CDATA, disclosed as bundle="compact" bodies="0" reason="compact-route". Read the map, then --expand=SYM the one you want. --auto-bodies restores the body walk there. That shape discloses on every run too: a ceiling the signatures alone exhaust carries bundle="compact" bodies="0" reason="budget" — three distinct reasons, never collapsed (compact-route = the route chose edges, no_candidates = nothing scored, budget = the ceiling was spent). An explicit --token-budget=N is a hard ceiling, split so a wider ceiling never buys less: the signature side's claim is capped at the DEFAULT ~7.5KB sig budget and every byte beyond it flows to the enrichment — at any ceiling at or above the default's effective total the block is byte-identical to the default run's, so every body (or hop row) the default serves still fits. An explicit --pack-top-n is an explicit SIG posture and keeps the whole-ceiling sig claim. --compress composes: the served bodies (auto/anchor and --detail=N alike) go through the same comment-strip --expand uses, disclosed as compress="1" on the element (nothing to strip on the compact route). RANKING CONFIDENCE, disclosed not scored: the root always carries confidence="high|low" margin_pct="N" — derived from the SAME relevance-cliff gap statistic --adaptive cuts at (no new scorer, no behavior change; the --json dialect carries the same two keys). low means the ranking is FLAT (no material score cliff and more positive matches than the head shows) — treat the set as a starting point, not an answer; high means a material cliff inside the served head (margin_pct= is that drop as a whole percent) or every positive match already shown
**Try it**
_Name-shaped query: the router picks name-exact BM25 (header says which/why)._
```
$ ./build/ripwire . --for="rankGraphTeleport"
PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line]
#### The convergence disclosure contract … [line truncated: 85 more bytes on this line]
... [18 more line(s); run it to see the whole thing]
```
**Shaped by:** `--top-k`, `--token-budget`, `--signatures-only`, `--auto-bodies`, `--no-route`, `--adaptive`, `--no-mention-boost`, `--no-doc-mention`
**Caveats (stated by the binary):**
- marks it: T rows handed to the trim, S printed).
- TERMINAL BY DEFAULT: after the signatures, the top-ranked symbols' FULL bodies ride inline (CDATA + callee signatures, the --expand shape) under a fixed extra body allowance — whole-body-or-not-at-all, rank-first, capped at the --pack-task candidate cap (6).
- ANCHOR-ONLY when the route names one: a query that NAMES a symbol gets THAT symbol's own body or NO body — never a same-named doc section, type stub or re-export shim from another file standing in for it.
### `--signatures-only`
**Answers:** (with --for) opt out of the terminal-by-default bundle: no auto bodies, no bundle="auto" attribute — the signatures-only lens exactly as before.
Contradicts --detail=N (refused together); --detail=N remains the explicit body knob and supersedes the automatic pick
**Try it**
_T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above._
```
$ ./build/ripwire . --for="rankGraphTeleport" --signatures-only
PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line]
#### The convergence disclosure contract … [line truncated: 85 more bytes on this line]
```
**Shaped by:** `--for`, `--auto-bodies`
**Caveats (stated by the binary):**
- Contradicts --detail=N (refused together);
### `--auto-bodies`
**Answers:** (with --for) opt out of COMPACT conceptual serving: restore the rank-first auto walk on the subtoken+body route (bundle="auto", up to 6 full bodies) instead of the edge section.
Inert on the name-exact route, where the allowance already runs. Contradicts --signatures-only and --detail=N (refused with either)
**Try it**
_Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto")._
```
$ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies
The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024uTSTree* parseTree( TSParser* parser, std::string_view src )inline DoctorGrammarProbe doctorProbeGrammars()errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealthenum class AstWalk : std::uint8_t
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--for`
**Caveats (stated by the binary):**
- Inert on the name-exact route, where the allowance already runs.
- Contradicts --signatures-only and --detail=N (refused with either)
### `--no-route`
**Answers:** (with --for/--query) force plain subtoken+body BM25.
Routing is now the DEFAULT: a deterministic, confidence-gated query-shape router picks name-exact BM25 when the query NAMES a symbol (identifier syntax, or every content word is a symbol name) else subtoken+body, and prints which/why in the header. It only routes with a query (the plain map is unaffected). --no-route restores the old behavior. A name-exact header also names its EVIDENCE: anchors: word(defining/file) per anchoring word, +N when N further definitions share that name, or word(syntax) when the word routed on camel/snake SHAPE and names nothing. Paths deeper than two segments print top/.../basename. Discount a one-use test helper yourself. Routing also carries the QUERY-SHAPE document demotion: when the task text parses as a stack trace, sanitizer report or compiler diagnostic, or as a pasted issue-template form, the DOCUMENT tier scores down (repo meta-prose - issue templates, CONTRIBUTING, changelogs - twice as hard) and route= names the shape, its evidence and both factors. Demotion, never exclusion, and the mention anchor still lifts a document the task NAMES. --no-route has no route= to disclose it in, so it does not demote either.
**Try it**
_Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run._
```
$ ./build/ripwire . --for="rankGraphTeleport" --no-route
uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f )PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f )inline ChurnRanking churnRankedGraph( const MainDispatch& d )L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c…
... [20 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- Demotion, never exclusion, and the mention anchor still lifts a document the task NAMES.
### `--adaptive`
**Answers:** (with --for/--query) cut the result at the relevance CLIFF — the largest relative score gap (Adaptive-k), floor 5, ceiling = the existing top-k;
a sharp query returns few, a flat/broad one hits the ceiling. Prints [adaptive: kept K of N ...] in the header. Without it, output is unchanged.
**Try it**
_Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N])._
```
$ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive
The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024uTSTree* parseTree( TSParser* parser, std::string_view src )inline DoctorGrammarProbe doctorProbeGrammars()errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealthenum class AstWalk : std::uint8_t
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--for`, `--detail`
**Caveats (stated by the binary):**
- (with --for/--query) cut the result at the relevance CLIFF — the largest relative score gap (Adaptive-k), floor 5, ceiling = the existing top-k;
### `--no-mention-boost`
**Answers:** (with --for) disable the query-mention anchor.
By DEFAULT, a file, dotted module, or Scope.symbol literally NAMED in the task text (a path, `pkg.module`, `Type.method` — even inside a URL) has its SCORE lifted to within 5% of the top score; the header says what anchored. That is a score promise, not a rank one: on a flat/tied head the anchored hit can still land several ranks below #1. Inert (byte-identical) when the text names nothing indexed. RIPWIRE_NO_MENTION=1 disables it everywhere (incl. MCP `for`).
**Try it**
_Same task with the anchor disabled — the contrast the flag exists for._
```
$ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost
P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote()inline int runEvalRetrieval( const IngestResult& ing, const Graph& g )The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul )
... [21 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- That is a score promise, not a rank one: on a flat/tied head the anchored hit can still land several ranks below #1.
- Inert (byte-identical) when the text names nothing indexed.
### `--no-doc-mention`
**Answers:** (with --for) disable doc-mention surfacing.
By DEFAULT, a markdown doc that names one of the task's top-resolved symbols in a `backtick` (the same doc<->code edges --mentions=SYM reads) is lifted into the bundle, strictly below that symbol's own score — closing the "the doc explains it but shares no words with the query" gap. Inert (byte-identical) when no resolved symbol has a mentioning doc. RIPWIRE_NO_DOC_MENTION=1 disables it everywhere (incl. MCP `for`/`pack_task`).
**Try it**
_The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row)._
```
$ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention
inline QualityDeltaOutcome computeQualityDelta( const std::string& root )std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line]
runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d )WHAT this delta is measured AGAINST, and on WHICH tree — the one place --quality-delta decides…struct DeltaBasis
... [21 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- Inert (byte-identical) when no resolved symbol has a mentioning doc.
### `--lego=TYPE`
**Answers:** the interface->impls view for ONE named interface/base: its signature, method contract, and every implementor (own-language only).
file:name disambiguates a same-named type. No contract for a language this surface cannot read soundly: methods=0 caveat=… says so.
**Try it**
_Interface -> implementors view: every existing impl of the named interface; the method contract is extracted for the C-family/Java/TS/Python tiers — for a Rust trait (this fixture) it discloses caveat="not-extracted-for-lang" rather than an empty list._
```
$ ./build/ripwire . --lego=Vehicle
```
**Shaped by:** `--callers`, `--expand`, `--layout`
**Caveats (stated by the binary):**
- file:name disambiguates a same-named type.
- No contract for a language this surface cannot read soundly: methods=0 caveat=… says so.
### `--exemplar=TASK|KIND`
**Answers:** before you write: the repo's best-in-class instance to IMITATE.
Just pass a plain task — --exemplar="format byte sizes" — and the KIND is inferred from the top match; or name a KIND directly (fn|method|class|struct|iface|var). Picks by ROLE — lowest cognitive cx under a hard ccx ceiling, then tested + highest fan-in; test-fixture paths de-prioritized — NOT text similarity (similar-snippet retrieval measurably hurts). A weak task match falls back to fn (low_confidence=1); an all-over-ceiling kind flags over_ccx_bar=1
**Try it**
_The repo's best-in-class instance to imitate before writing new code (picked by ROLE)._
```
$ ./build/ripwire . --exemplar="format byte sizes for humans"
```
**Shaped by:** `--compress`, `--metrics`, `--doctor`, `--limit`, `--index-out`
### `--recall=TASK`
**Answers:** recall the most relevant DOCS — memory/plans/designs, full bodies (md, .ipynb/.html/.csv, plus Office/PDF via the optional markitdown bridge).
This is the tool's LARGEST output: its header reports est_tokens + total=/shown=/capped=, where total= is the TRUE relevant count (score > 0) and shown= is what this run actually emitted. The header's "of N document files" denominator counts every file the index carries as a DOCUMENT — .md plus the docparse'd .ipynb/.html/.csv — so it is a SUPERSET of --doc-drift's docs=, which is an extension test (markdown only). Two populations, two names, deliberately. --top-k=N shapes HOW MANY docs are emitted (default 8, not the general --top-k default of 200). Recall defaults to an 8000-token body ceiling; --max-tokens=N overrides it and shapes to fit (disclosing each cut), while --token-budget=N gates the finished artifact (exit 3, nothing streamed). GENERATED documents rank LAST by default — a doc that declares itself generated in its first lines, or is BOTH >=5x the median doc's size AND mostly ```-fenced quoted output (a capture/API dump quotes every term, so BM25 hands it every query). Never dropped: it still wins when nothing else matches. Each one says [generated_demoted: marker|size+fences] on its own line and the header tallies generated_demoted=N
**Try it**
_Most relevant DOCS' full bodies (markdown only) — recall what is already written down._
```
$ ./build/ripwire . --recall="quality delta gating exit codes"
ripwire recall — "quality delta gating exit codes" — 74 relevant of 150 document files, best-first — total=74 shown=8 capped=1 truncated=5 generated_demoted=1 max_tokens=8000 share_bytes=2278 est_tokens=5593 [lines= on a doc is its SELECTED section range — pre-truncation; the per-doc trunca … [line truncated: 45 more bytes on this line]
━━ skills/ripwire-quality-bar/SKILL.md (relevance 6.571) ━━ [sections: 8 of 10, section-granular; whole doc 31569 B; lines="54-137,138-258,259-287,288-308,309-340,341-350,351-361,362-367"] [truncated: 1817 of 27368 bytes]
## Before you converge: the wide-angle read — `--quality-panel`
`ripwire --quality-panel[=strict|default|lenient]` is THE SINGLE COMMAND for "does what I just
touched still look rotten" — one ranked report over **six** evidence families (the four `--ensemble`
joins — `structural`, `lexical`, `confusion`, `historical` — plus `colocation` and `state`; the full
per-family breakdown lives in **ripwire-fresh-eyes**). Point it at the file or symbol you just edited for
a multi-angle second opinion the single `--quality-delta` number can't give you on its own.
**Read it correctly: it is a lens, never a gate.** `--help` says so in the flag's own text and the
contract is enforced in code — `--quality-panel` exits 0 unconditionally, on every preset, on every repo.
It does not compare against a baseline and it cannot fail a commit. The gate for "did MY change make this
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--max-tokens`, `--token-budget`, `--no-redact`, `--from-trace`, `--legend`, `--limit`
**Caveats (stated by the binary):**
- This is the tool's LARGEST output: its header reports est_tokens + total=/shown=/capped=, where total= is the TRUE relevant count (score > 0) and shown= is what this run actually emitted.
- Never dropped: it still wins when nothing else matches.
### `--tree`
**Answers:** file-by-file orientation map (top symbols per file).
Default window: the 80 files with the best-ranked symbols (shown=/capped=/total=/next_offset= disclose the cut, next= pastes the next page); --limit=N/--offset=M window it explicitly (--limit=100000 = every file)
**Try it**
_File-by-file orientation map (top symbols per file)._
```
$ ./build/ripwire . --tree
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- Default window: the 80 files with the best-ranked symbols (shown=/capped=/total=/next_offset= disclose the cut, next= pastes the next page);
- --limit=N/--offset=M window it explicitly (--limit=100000 = every file)
### `--html[=FILE]`
**Answers:** self-contained HTML force-directed call graph of the DEFAULT map (no CDN — redirect or write FILE).
A navigation or report verb answers instead of the map, so --html beside one refuses rather than writing nothing. The page OPENS on the whole selected map, so the run is the picture; FILE#node/SYM/2 opens one symbol's neighbourhood and FILE#overview the module (community) list
**Try it**
_Self-contained HTML force-directed call graph._
```
$ ./build/ripwire . --html=/aux/map2.html
(empty)
```
**Shaped by:** `--color-by`, `--legend`
**Caveats (stated by the binary):**
- A navigation or report verb answers instead of the map, so --html beside one refuses rather than writing nothing.
### `--color-by=MODE`
**Answers:** (with --html) node colour: lang (default) | community | cx | churn | tested — the page embeds all five and keeps a live selector;
the flag only sets the initial mode
**Try it**
_The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector)._
```
$ ./build/ripwire . --html=/aux/map2.html --color-by=community
(empty)
```
### `--order=MODE`
**Answers:** emit order: stable (path/id order — provider KV-cache hits across re-runs) | important-first (rank order, the default;
no auto-flip) | important-last (highest-rank content emitted last — recency bias for an LLM). Large default maps auto-flip to important-last past ~50% of a nominal 32K window (est_tokens>16000) unless MODE is explicitly given.
**Try it**
_Stable (path/id) emit order — provider KV-cache hits across re-runs._
```
$ ./build/ripwire . --order=stable --top-k=5
... [6 more line(s); run it to see the whole thing]
```
**Shaped by:** `--no-stable`
### `--no-stable`
**Answers:** opt out of the stable ordering that --mcp/--listen enable by default.
Read ONLY there: on the CLI it changes nothing and says so on stderr (the map is important-first unless you pass --order=stable)
**Try it**
_--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of._
```
$ ./build/ripwire . --no-stable --top-k=3
... [4 more line(s); run it to see the whole thing]
```
---
## navigate / answer a question
### `--around=SYM`
**Answers:** ego graph around SYM [--around-depth=N, default 1] [--around-fanout=K, default 32] (default depth 1 since 2026-09-05: depth 2 was 3x the whole default map on this repo;
the root's depth= says which; --around-depth=2 restores the 2-hop neighbourhood) the root echoes all three (of= depth= fanout=), so the boundary of what could appear is readable and, when a bound actually CUT, which one: depth_truncated="1" (a symbol one hop past depth= is absent) / fanout_cut="N" (N distinct symbols the fanout cap dropped, absent from the whole answer, exact not a floor). Neither is emitted when its bound cut nothing, so absent = the bound did not bind and raising it would return nothing new
**Try it**
_Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo._
```
$ ./build/ripwire . --around=rankGraphTeleport
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--callers`, `--layout`, `--limit`
### `--callers=SYM`
**Answers:** who calls SYM (1-hop in-edges).
file:name disambiguates a same-named symbol across files (like --around/--lego); Scope::name picks one scope's definition — the sym= spelling edit-check prints resolves everywhere
**Try it**
_Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance._
```
$ ./build/ripwire . --callers=DoesNotExist
(empty)
```
**Shaped by:** `--callees`, `--uses`, `--impact`, `--expand`, `--edit-check`, `--slice-flow`, `--at`, `--format`
**Caveats (stated by the binary):**
- file:name disambiguates a same-named symbol across files (like --around/--lego);
### `--callees=SYM`
**Answers:** what SYM calls (1-hop out-edges).
file:name disambiguates like --callers
**Try it**
_What SYM calls (1-hop out-edges)._
```
$ ./build/ripwire . --callees=rankGraphTeleport
```
**Shaped by:** `--impact`, `--exercises`, `--format`, `--json`, `--limit`
**Caveats (stated by the binary):**
- file:name disambiguates like --callers
### `--uses=SYM`
**Answers:** the statically resolvable use-sites of SYM (role=call|macro|read|write|import|extends|type, file:line);
external="1" if SYM has no in-corpus def. file:name narrows defs= AND the role="call" sites (kept only where the call RESOLVES to a chosen def — --callers' own narrowing); read/write/import/extends carry no resolution and stay name-matched. narrowed_roles=/defs_of_name=/call_sites_of_name= (file: qualifier only) disclose what narrowed and the un-narrowed totals; a file: qualifier naming a file with no such def REFUSES, like --callers/--impact Owner.field (also Owner::field, or the id=) — a MEMBER VARIABLE's own use-sites, RESOLVED per site: this->f/self.f/bare f inside the owner pin; v.f pins through v's recorded type, else every owner is a candidate and the row carries amb=K (never a silent pin); write = assignment/compound/++ (address-of and by-reference passing are NOT claimed). A bare field name shared by several owners REFUSES with the Owner.field spellings; C/C++/Python fields only, others refuse
**Try it**
_The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor._
```
$ ./build/ripwire . --uses=rankGraphTeleport
```
**Shaped by:** `--impact`, `--naming-consistency`, `--edit-check`, `--edit-target-file`, `--safe-delete`, `--doctor`, `--format`, `--limit`
**Caveats (stated by the binary):**
- a file: qualifier naming a file with no such def REFUSES, like --callers/--impact Owner.field (also Owner::field, or the id=) — a MEMBER VARIABLE's own use-sites, RESOLVED per site: this->f/self.f/bare f inside the owner pin;
- v.f pins through v's recorded type, else every owner is a candidate and the row carries amb=K (never a silent pin);
- A bare field name shared by several owners REFUSES with the Owner.field spellings;
### `--graph-query=EXPR`
**Answers:** composable node-set query over the call graph: sources name("X")/all;
filters kind|cx|fanin|file|layer; bounded closure callers|callees(SET[,depth]); joins and|or|not. e.g. and(callers(name("foo"),2),kind(all,fn)); file() regex example: file("src/.*\\.cpp") (or in bash, use single quotes: file('src/.*\.cpp')) layer(SET,NAME) keeps the architecture layer NAME (game|infra|render|math|audio|ai|test) — the SAME built-in directory-name taxonomy the map prints as layer= on a file node, so the two cannot disagree. It does NOT read a --arch=FILE rules file: --arch is a verb and outranks --graph-query, so the two never run together. An unknown layer word, or ANY layer() against a tree where no path names a layer, is REFUSED (exit 1) rather than answered count="0" — 0 there would read as "no such code". a name("X") literal matching NO indexed symbol refuses with a did-you-mean (a typo is not a count=0); a query whose names all resolve but that selects nothing still reports count="0" — that IS a measurement (including a VALID layer with no members in a tree that does have layers). Ranked result set is capped at --top-k (default 200); --limit overrides that cap (raise or lower it), --offset pages past it — see --limit=N --offset=M above
**Try it**
_Composable node-set query: functions within 2 caller-hops of rankGraphTeleport._
```
$ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--exercises`, `--limit`
**Caveats (stated by the binary):**
- file() regex example: file("src/.*\\.cpp") (or in bash, use single quotes: file('src/.*\.cpp')) layer(SET,NAME) keeps the architecture layer NAME (game|infra|render|math|audio|ai|test) — the SAME built-in directory-name taxonomy the map prints as layer= on a file node, so the two cannot disagree.
- It does NOT read a --arch=FILE rules file: --arch is a verb and outranks --graph-query, so the two never run together.
- An unknown layer word, or ANY layer() against a tree where no path names a layer, is REFUSED (exit 1) rather than answered count="0" — 0 there would read as "no such code".
### `--external-surface`
**Answers:** names referenced but never defined in-corpus (the stdlib/third-party surface), by ref count;
each row's lang= is the REFERENCING file's language — a name called from several languages (e.g. printf: C stdio call vs Bash builtin) gets one row PER language, not a merged count. Default window: 100 rows (shown=/capped=/next_offset=, next= pastes the next page; --limit=N raises it) and the sh BUILTINS (echo printf cd exit test …) are dropped — builtins_excluded= counts them; --include-builtins keeps them
**Try it**
_Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface._
```
$ ./build/ripwire . --external-surface
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- names referenced but never defined in-corpus (the stdlib/third-party surface), by ref count;
- printf: C stdio call vs Bash builtin) gets one row PER language, not a merged count.
- Default window: 100 rows (shown=/capped=/next_offset=, next= pastes the next page;
### `--path=SRC,DST`
**Answers:** shortest directed call-path SRC -> DST
**Try it**
_Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0")._
```
$ ./build/ripwire . --path=main,rankGraphTeleport
```
**Shaped by:** `--connect`, `--limit`
### `--connect=A,B,C`
**Answers:** minimal connecting subgraph over 2..16 symbols: terminals + fewest joining intermediaries + call edges in TRUE direction (finds the shared-caller join a directed --path can't) [--connect-radius=N (1..12, default 6)]
**Try it**
_Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._
```
$ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex
```
**Shaped by:** `--no-redact`, `--from-trace`, `--limit`
### `--impact=SYM`
**Answers:** transitive blast radius — the indexed symbols that reach SYM (a floor, see counts_floor).
file:name disambiguates like --callers importers= is a SECOND, weaker reach beside it: the files that directly include/import a file defining SYM, emitted as rows (format=columnar carries the count only). NEVER added to reaches= — files and symbols are different units, and an importer may use a different symbol from that file, or none at all. lazy="1" (TS/JS only): every one of that importer's edges is a require()/import() written inside a function body, not at module load time — still a real dependency, weaker than a top-level one counts_floor="1" on --callers/--callees/--uses/--impact/--edit-check every count is a FLOOR, never a total: the call graph is extracted from source text by name, so dynamic dispatch and declarations that parse without a call expression (C++ most-vexing-parse) contribute no edge; a call through a function pointer/callback is an edge only when ONE function is bound to that variable in scope (reassigned/table-indexed/lambda-bound/escaped — address-taken or reference-bound — pointers stay edge-less, C-family); a macro-generated call site contributes a role="macro" edge when its name uniquely names an indexed function-like #define (t="macro"); a shared name stays a plain call, an unindexed macro's site is no edge. Read a 0 as "none found", never as "none exists". Those five verbs also count DISTINCT (caller,callee) pairs, while --uses counts call SITES — see each verb's own legend pr_iters="N" on every PageRank-ordered document (the map, and the tree, seams, communities, zoom, impact, graph-query and exercises verbs, plus their MCP twins): how many power iterations produced that ordering. The iteration stops when the L1 residual between successive rank vectors falls below tolerance, or at a fixed iteration ceiling, whichever comes first. pr_converged="0" is emitted ONLY on that second exit and means the ranking is a rank vector that stopped SHORT of tolerance, not the fixed point it approximates. ABSENCE MEANS IT CONVERGED (there is no pr_converged="1": the converged path is the normal one and must cost zero bytes), and absence of pr_iters= itself means the document was not ordered by a power iteration at all (a lexical query score, or a hub or authority HITS vector), never that the count is unknown
**Try it**
_Transitive blast radius — everything that reaches SYM. NOW carries shown/capped._
```
$ ./build/ripwire . --impact=rankGraphTeleport
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--uses`, `--metrics`, `--safe-delete`, `--slice-flow`, `--format`, `--json`, `--limit`
**Caveats (stated by the binary):**
- transitive blast radius — the indexed symbols that reach SYM (a floor, see counts_floor).
- file:name disambiguates like --callers importers= is a SECOND, weaker reach beside it: the files that directly include/import a file defining SYM, emitted as rows (format=columnar carries the count only).
- NEVER added to reaches= — files and symbols are different units, and an importer may use a different symbol from that file, or none at all.
### `--verify="CLAIM"`
**Answers:** VERIFY A CLAIM about the code in ONE call: a CLOSED claim language in, a three-valued verdict out (confirmed / refuted / not-established) with the evidence rows inline — the collapse of the manual verification grep-chain.
Shapes: calls(A,B) does A transitively call B; uses(SYM) / unused(SYM) is SYM referenced anywhere / nowhere; contains(FILE, "LIT") do FILE's indexed bytes contain the literal; defines(FILE, SYM) does FILE define SYM; reaches(SYM, "FILE"|LAYER) does code in that file/layer transitively call SYM (LAYER unquoted: game|infra|render|math|audio|ai|test). refuted appears ONLY with complete evidence: a clean literal-scan absence carries complete=, and an unused claim is refuted by printed witness sites. A graph or reference ZERO can never refute — it yields not-established with limit= naming the floor (call-graph-floor, reference-floor, collection-ceiling, scan-degraded, extraction-floor); see counts_floor above for why. An unknown shape refuses loudly with the whole vocabulary; SYM takes the shared selector grammar (name, file:name, Scope::name, canonical id), FILE is a path substring
**Try it**
_An unparseable claim — the refusal names the accepted shapes._
```
$ ./build/ripwire . --verify="frobnicate(x)"
(empty)
```
**Caveats (stated by the binary):**
- A graph or reference ZERO can never refute — it yields not-established with limit= naming the floor (call-graph-floor, reference-floor, collection-ceiling, scan-degraded, extraction-floor);
- see counts_floor above for why.
- An unknown shape refuses loudly with the whole vocabulary;
### `--mentions=SYM`
**Answers:** markdown docs (plans/designs) that name SYM in a `backtick` (doc↔code).
An @FILE:LINE seed rebinds to the innermost enclosing definition and answers, disclosing sym= the pre-PR family — plumbing (--affected) to mid-task report (--situ) to gate (--test-gate):
**Try it**
_Markdown docs that name SYM in a backtick (doc<->code edges)._
```
$ ./build/ripwire . --mentions=rankGraphTeleport
```
**Shaped by:** `--no-doc-mention`, `--at`, `--limit`
### `--affected=F1,F2|SYM`
**Answers:** test files that transitively reach the changed files -- or the changed SYMBOL.
Each item may be `path`, `./path`, `path:LINE` / `path:N-M` (paste a --hotspots/--clones/--grep/--lint/ --quality-delta row's locator verbatim; the trailing line locator is stripped, same for --situ/--test-gate), or a symbol: `NAME`, `file:NAME`, `path::scope::name`. FILE-FIRST: an item matching any indexed path is a PATH pattern (unchanged semantics -- `--affected=widget` stays the ./src/widget.cpp pattern); only an item matching NO indexed path is offered to the symbol resolver, and `file:NAME` reaches the symbol reading explicitly. seeded_by="file|symbol|mixed" + seeds=N report which reading fired and how many defs it seeded. An item matching NEITHER refuses (exit 1) naming both readings. seed_test_files=N of the matched files are TEST files: a test cannot reach a change it is part of, so its own symbols are not seeds of the caller walk, and its row carries seed_kind="test" -- it is listed because the argument matched it (it changed, run it), not because it reaches. script_gates_unmodelled= counts the script runners under test/, recursively (a path count; not every one invokes the binary) that this call's graph walk cannot see either way (script-to-binary is not a call edge) — a corpus-wide fact, not scoped to the changed set given
**Try it**
_Test files that transitively reach the changed file._
```
$ ./build/ripwire . --affected=src/graph.h
```
**Shaped by:** `--mentions`, `--exercises`, `--test-gate`, `--edit-target-file`
**Caveats (stated by the binary):**
- An item matching NEITHER refuses (exit 1) naming both readings.
- seed_test_files=N of the matched files are TEST files: a test cannot reach a change it is part of, so its own symbols are not seeds of the caller walk, and its row carries seed_kind="test" -- it is listed because the argument matched it (it changed, run it), not because it reaches.
- script_gates_unmodelled= counts the script runners under test/, recursively (a path count;
### `--exercises=TESTFILE`
**Answers:** the INVERSE of --affected: the non-test symbols this test file transitively calls into -- what it actually covers.
The first question when a test fails and you have its name and nothing else. Ranked by PageRank, capped at 40 rows (raise with --limit; --offset pages). A NON-TEST path REFUSES rather than answering generically: this verb IS the test/non-test partition (it subtracts test code from the answer), which means nothing for a non-test file -- for "what does this call", use --callees=SYM or --graph-query callees(...) A shell harness carries harness=script: subprocess coverage is unmodelled, so reaches=0 there is a stated limit, not a measurement (the inverse of script_gates_unmodelled).
**Try it**
_Which symbols a TEST FILE exercises — the reverse direction of --affected._
```
$ ./build/ripwire . --exercises=test/regression.sh
```
**Shaped by:** `--test-gate`, `--limit`
**Caveats (stated by the binary):**
- Ranked by PageRank, capped at 40 rows (raise with --limit;
### `--situ[=F1,F2]`
**Answers:** situational awareness for a change: blast radius + tests + co-change (default = git diff)
**Try it**
_Mid-task situational report for the current git diff — recorded against a DIRTY tree (contrast with the sandbox run below)._
```
$ ./build/ripwire . --situ
ripwire situational-awareness — 0 changed file(s), 0 symbols in them
root: .
at: 5ca98897b+dirty
(0 changed files — working tree is clean, nothing to analyze)
```
**Shaped by:** `--top-k`, `--mentions`, `--affected`, `--test-gate`, `--legend`
### `--handoff`
**Answers:** continuation packet for the NEXT session: disk truth (branch/sha, changed files+symbols, blast radius, tests-to-run) + labeled suggestions (co-change partners, committed notes, plan/design doc pointers via a branch+commit-subject query).
Empty diff is fine — the packet still carries branch/sha + heuristics. Composes with --token-budget=N (drops heuristic rows tail-first, disclosed as withheld= in the header; verified rows are never dropped). Single-root only.
**Try it**
_The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a DIRTY tree._
```
$ ./build/ripwire . --handoff
```
**Caveats (stated by the binary):**
- continuation packet for the NEXT session: disk truth (branch/sha, changed files+symbols, blast radius, tests-to-run) + labeled suggestions (co-change partners, committed notes, plan/design doc pointers via a branch+commit-subject query).
- Empty diff is fine — the packet still carries branch/sha + heuristics.
- Composes with --token-budget=N (drops heuristic rows tail-first, disclosed as withheld= in the header;
### `--test-gate[=F1,F2]`
**Answers:** agent self-check before a PR (pair with --quality-delta): names the tests to run + the UNTESTED blast radius;
exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff) run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries run="" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem matches the harness's, or whose TEXT names the harness file. Spelled with the same root you scanned, so it pastes straight into a shell. NO run= means NOT DERIVABLE -- never a guessed suite command
**Try it**
_Pre-PR gate recorded against a DIRTY tree, so the obligations below are the working copy's real ones — the recorded exit code says which way it went._
```
$ ./build/ripwire . --test-gate
```
**Shaped by:** `--mentions`, `--affected`, `--quality-delta`, `--edit-target-file`, `--json`, `--limit`
**Caveats (stated by the binary):**
- NO run= means NOT DERIVABLE -- never a guessed suite command
### `--grep=STR | --regex=PAT`
**Answers:** literal / regex search + enclosing symbol + the matched line.
SPAN-TIERED by default (see --grep-in below): the scan itself is exhaustive, the ANSWER serves one tier and discloses what it held back. --grep-in=any is the exhaustive VIEW -- every hit, no tiering. For task-ranked retrieval use --for=TASK (ranks by PageRank + task relevance).
**Try it**
_Regex search + enclosing symbol._
```
$ ./build/ripwire . --regex='fnv1a\w+'
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--and`, `--not`, `--grep-scope`, `--handles`, `--expand`, `--no-redact`, `--insert-after-symbol`
### `--grep-context=N | --grep-before=N / --grep-after=N`
**Answers:** ripgrep-style N lines of source around each hit
**Try it**
_Same search with one line of source context either side._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-context=1
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--no-redact`
### `--and=STR`
**Answers:** (repeatable) modifies --grep=STR: keep only hits where STR is ALSO present (literal-only, no --regex)
**Try it**
_Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default)._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--grep-scope`
### `--not=STR`
**Answers:** (repeatable) modifies --grep=STR: drop hits where STR IS present (literal-only, no --regex)
**Try it**
_Drop every hit in a file that ALSO contains the --not literal anywhere (file scope)._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--grep-scope`, `--no-redact`, `--legend`
### `--grep-scope=line|file`
**Answers:** modifies --and=/--not=: line (default) requires the SAME matched line;
file requires anywhere in the same file. Second occurrence of --grep=/--regex= itself REFUSES (naming --and= as the AND spelling) rather than silently overwriting the pattern.
**Try it**
_Drop every hit in a file that ALSO contains the --not literal anywhere (file scope)._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file
... [17 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- Second occurrence of --grep=/--regex= itself REFUSES (naming --and= as the AND spelling) rather than silently overwriting the pattern.
### `--grep-in=code|any`
**Answers:** SPAN TIERS: which tree-sitter span a hit must sit in to print.
code (default) serves the CODE tier when any hit is code, and otherwise comment AND string TOGETHER (tier= "comment+string"), disclosing what it held back (suppressed_comment=/suppressed_string=); a pattern living only in prose is still answered, never silently emptied. any turns tiering off entirely -- the exhaustive view. Hit files are parsed on demand under a fixed budget; tier_budget= says so when it stops, and hits it never classified are emitted, never suppressed.
**Try it**
_Span tiers off: the exhaustive view — the comment and string hits the default tier held back (suppressed_comment=96 / suppressed_string=29 in the plain --grep block above) now print alongside the code hits; hits= grows accordingly._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-in=any
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--grep`
**Caveats (stated by the binary):**
- a pattern living only in prose is still answered, never silently emptied.
- tier_budget= says so when it stops, and hits it never classified are emitted, never suppressed.
### `--handles`
**Answers:** (with --grep/--regex) add h= to each unique editable enclosing-symbol row: a stable identity plus the file-content hash pinned when grep ran.
Ambiguous or document-only rows get no handle; a later edit must refuse after any file change rather than retarget stale coordinates.
**Try it**
_h= on each editable enclosing-symbol row: a freshness-pinned identity an edit verb can target and must refuse on after any file change._
```
$ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --handles
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--insert-after-symbol`
**Caveats (stated by the binary):**
- Ambiguous or document-only rows get no handle;
- a later edit must refuse after any file change rather than retarget stale coordinates.
### `--match=QUERY`
**Answers:** tree-sitter structural (shape) query
**Try it**
_Tree-sitter structural query WITHOUT a capture — a bare node query gets a capture AUTO-ADDED (auto_captured="1") and matches the same nodes the explicit form does._
```
$ ./build/ripwire . --match='(if_statement)'
if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecteif not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasksif train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["reif not repos: return 0.0, []if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0if bv: ratios.append( cv / bv - 1 )if not ratios: return None, Noneif rw is None or native is None: return None
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--no-redact`, `--sarif`, `--limit`
### `--pattern=PAT`
**Answers:** structural search written in CODE, not in node kinds: --pattern='foo($X, ...)'.
$NAME binds one node (repeat it and both sites must match structurally); $_ binds nothing; ... (or $$$) is an ellipsis over siblings, matched by ONE first-match-wins probe under a hard cap -- both facts on the element. Comments are transparent, everything else is kind- and text-exact ($A + $B does not match a - b). Served: c cpp objc java csharp javascript typescript python go rust swift; ruby, bash and the data tiers are named in unsupported= instead of answered. A pattern no served grammar resolves, or that collapses to a bare token, is REFUSED -- never reported as hits=0.
**Try it**
_A pattern that collapses to a bare token is REFUSED — never reported as hits=0._
```
$ ./build/ripwire . --pattern='x'
(empty)
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- A pattern no served grammar resolves, or that collapses to a bare token, is REFUSED -- never reported as hits=0.
### `--query=TERMS`
**Answers:** raw BM25 ranking (debug);
use --for
**Try it**
_Raw BM25 ranking (debug lens; --for is the real verb)._
```
$ ./build/ripwire . --query="teleport pagerank" --top-k=5
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--top-k`, `--token-budget`, `--no-route`, `--adaptive`, `--format`
---
## zoom the detail ladder
### `--detail=N`
**Answers:** (with --for) importance-weighted detail: FULL bodies for the top-N ranked symbols + signatures for the rest, in ONE call — spend body tokens only on the head the rank identifies.
Composes with --max-tokens (bounds the bodies) and --adaptive. 0 = off.
**Try it**
_Importance-weighted detail: FULL bodies for top-2, signatures for the rest._
```
$ ./build/ripwire . --for="pagerank power iteration" --detail=2
HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix ent … [line truncated: 42 more bytes on this line]
Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as )What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosureWhat a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--for`, `--signatures-only`, `--auto-bodies`, `--compress`, `--owners`, `--plan`, `--abi`, `--flip`
### `--pack-signatures`
**Answers:** body-elided decl skeletons — ~72-90% fewer element bytes than the same symbols' full --expand bodies (roughly 80% at the top-50 sigs payload cap — the sigs payload is top-50 whatever --top-k is set to, and --top-k's own default is 200), measured at top-10/50/100 on this repo with the corpus-root prefix subtracted from both sides: that prefix repeats inside every element, is charged in both forms, and is not what this elides — count it and the figure becomes a function of how deep your checkout sits.
test/showcasecapturecheck.sh (C) re-derives this range from the SAME repo every run and fails on drift. The share RISES with the result size. Like the --format=columnar sibling, a small result can invert it — a signature plus its doc comment can be bigger than a short body.
**Try it**
_Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 80.9% fewer bytes at top-10, 74.0% at top-50, 73.9% at top-100 (re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart._
```
$ ./build/ripwire . --pack-signatures --top-k=10
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--top-k`, `--limit`
### `--outline=A,B,...`
**Answers:** control-flow skeletons of A,B,...
(same selector grammar as --expand, minus the range)
**Try it**
_Control-flow skeleton of one symbol, payload-only via the new --top-k=0._
```
$ ./build/ripwire . --outline=rankGraphTeleport --top-k=0
& p, float alpha = 0.85f )
{
PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" );
const std::vector pw = biasPrior( g, p );
const std::size_t N = pw.size();
std::vector teleport( pw.begin(), pw.end() );
std::vector rankDouble( N, 0.0 );
PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun
if( N )
{
...
}
std::vector r( N, 0.f );
std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } );
... [3 more line(s); run it to see the whole thing]
```
**Shaped by:** `--top-k`, `--expand`, `--compress`, `--no-redact`, `--limit`
### `--expand=A,B,...`
**Answers:** full bodies of A,B,...
Selector grammar per item (the tail after the LAST ':' decides; a tail STARTING WITH A DIGIT is a range, anything else is a name): NAME every def of that name | FILE:NAME that file's def NAME:START-END body slice | FILE:NAME:START-END selector + slice FILE:LINE:NAME paste a row's p="path:line" straight from --callers/--lint/--grep (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its own p=/top_l=/top= instead, since top= is just the worst function's name) path::scope::name the canonical id= --for/--pack-task emit Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid); a wrong scope refuses, it never falls back to the bare-name union (START-END is 1-based within the def's OWN body — lines="lo-hi/total" marks the slice partial; out-of-range clamps. FILE matches any path substring, like --callers/--lego.) EXACT-NAME DEFAULT (one token, one unambiguous match, no explicit --top-k): the ranked map defaults to top-k=0 — you already named the exact symbol, so the ~200-row orientation map is pure overhead in front of the one body it exists to summarize. Disclosed on the root as topk_default="0" (self-describing: the change is visible without reading source). A MULTI-match name (an ambiguous bare name) or a multi-token --expand keeps the map — there IS something to disambiguate. An EXPLICIT --top-k=N (0 included) always overrides this default. Each body also carries sibs="a,b,..." sibs_total="N" [sibs_capped="1"] (the file's OTHER symbols, names only, capped at 40) and inc="x.h,..." inc_total="N" [inc_capped="1"] (the file's own #include/import targets, capped at 24) — both absent when the count is 0 (a documented zero, not a degrade), so a body no longer needs a second --outline call just to learn what else lives in its file. CHEAPEST-COMPLETE-ANSWER SERVING (no explicit --top-k, no range slice): the verb ALSO measures the (possibly map-less) bundle against the requested symbols' whole FILE(s) and emits the SMALLER, disclosed on the root as mode="bundle|whole-file" reason="the two byte counts" — on a small file the old bundle was 5.65x the file itself; on a big file the bundle saves ~26x. The whole-file form is with the file CDATA-wrapped (redacted as usual) and every requested symbol's line anchor kept. An EXPLICIT --top-k=N (including 0) opts out of BOTH the exact-name default and mode= auto-selection and keeps the classic undecorated shape; a SYM:START-END slice opts out of mode= auto-selection only (serving the whole file would invert an explicit narrowing) but still gets the exact-name top-k=0 default when it applies.
**Try it**
_NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for._
```
$ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--doctor`, `--json`, `--limit`, `--index-out`
**Caveats (stated by the binary):**
- also surfaces amp= (--metrics/--for/--exemplar): amp = |direct callers| (symbol-level, the in-edge CSR) + |co-change partners of the symbol's FILE| (file-level, mined from git history) — a deliberate GRANULARITY MIX, not a graph-only count;
- degrades to callers-only (still valid) when git/history is unavailable.
- amp= is DIRECT callers plus a historical co-edit signal the call graph cannot see at all — the two numbers on the same symbol routinely differ several-fold (one seen case: 4.6x apart) because they measure different things, not because one is wrong.
### `--deps`
**Answers:** file->file dependency graph (god-files, cycles — validated);
its nccd (Lakos) is a design heuristic, not independently outcome-validated. instab= (Martin's I=Ce/(Ca+Ce)) counts project includes ONLY -- system/third-party headers are excluded from Ce, matching stabledeps' gap= so gap == consumer's instab - provider's instab always. 's ccd/acd/nccd/shape are computed over dep_files= (files whose language has #include/import syntax) not files= (the raw corpus, incl. md/json/toml/yaml, which can't participate in the graph) -- --arch's propagation_cost uses the same N. names that language set, which is what makes a dep_files=/ccd/ acd/nccd number comparable across builds: sh, rb, lua and ex joined it at parser version 81 and every one of those numbers moved on a corpus holding them
**Try it**
_File->file dependency graph (god-files, cycles)._
```
$ ./build/ripwire . --deps
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--arch`, `--limit`
**Caveats (stated by the binary):**
- its nccd (Lakos) is a design heuristic, not independently outcome-validated.
### `--hotspots`
**Answers:** complexity x recent git churn (maintenance pain);
each row's top= is the worst function's BARE name, top_ccx= its cognitive complexity, top_l= its source line (build an --expand selector from p=/top_l=/top=, not from top= alone — it no longer carries a :line suffix)
**Try it**
_Complexity x recent git churn (maintenance pain)._
```
$ ./build/ripwire . --hotspots
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--expand`, `--since`, `--limit`
### `--clones`
**Answers:** token-normalized duplicate bodies
**Try it**
_Token-normalized duplicate bodies._
```
$ ./build/ripwire . --clones
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--limit`
### `--readability`
**Answers:** per-function readability lens, LEAST readable first: vol= Halstead volume V (N*log2(eta)), ent= Shannon token entropy E, lines= L, posnett= sigmoid(8.87 - 0.033V + 0.40L - 1.5E) (Posnett/Hindle/Devanbu, MSR 2011).
APPROXIMATION, disclosed: ONE token-class table serves every language (keywords + punctuation = operators, identifiers + literals = operands), with no per-grammar refinement, so V is cross-language and not a per-grammar Halstead count. The formula was fitted on snippets of 20 lines or fewer, so it is a RANKING lens, not a grade: read the ORDER of the rows, not the number on any one of them. Pages with limit=N (offset=M); default 40 rows. Declarations with no body are not measured.
**Try it**
_Per-function readability, LEAST readable first (Halstead volume, token entropy, lines, Posnett) — a RANKING lens, not a grade._
```
$ ./build/ripwire . --readability --limit=8
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- APPROXIMATION, disclosed: ONE token-class table serves every language (keywords + punctuation = operators, identifiers + literals = operands), with no per-grammar refinement, so V is cross-language and not a per-grammar Halstead count.
- The formula was fitted on snippets of 20 lines or fewer, so it is a RANKING lens, not a grade: read the ORDER of the rows, not the number on any one of them.
- Pages with limit=N (offset=M);
### `--nonlocal-state`
**Answers:** per function, the NON-LOCAL MUTABLE STATE it can reach, MOST WRITES FIRST: writes= reads= are the distinct cells this function OR its transitive callees write / read;
direct_writes= direct_reads= are the subsets in its own body. A cell is a file/namespace-scope variable, a function-local static, or a Python module global; a const/constexpr/consteval declaration is not a cell. Each cell child names its declaration, its direction (dir=r|w|rw) and either the use site in this body (at=) or the callee it came through (via=). Lineage: Fowler's Global Data / Mutable Data smells (2018) name the hazard and ship no metric; Marinescu's ATFD (ICSM 2004) is the closest number but is one-hop, per-class, Java, and direction-blind; QMOOD DAM and MOOD AHF/MHF count DECLARED VISIBILITY and so score a class with private fields and leaked mutable internals as perfectly encapsulated; the only published measurement of externally reachable state (Potanin/Noble/Biddle 2004) is DYNAMIC, Java-only, and its tool is unmaintained. UNSOUND BY CONSTRUCTION -- it cannot see indirect calls, pointer aliasing, macro-named cells or reflection-like dispatch, and a local SHADOWING a cell name can be charged to the cell -- so every count is a FLOOR (counts_floor="1") and the blind spots are listed in the report's own legend. COVERS C++, ObjC and Python -- the languages whose read/write USE SITES the index carries. Every other indexed language is named on the root as unanalyzed_langs= and contributes no cells and no rows: that absence is NOT a measured zero. Pages with limit=N (offset=M); default 40 rows.
**Try it**
_Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where._
```
$ ./build/ripwire . --nonlocal-state --limit=8
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- a const/constexpr/consteval declaration is not a cell.
- UNSOUND BY CONSTRUCTION -- it cannot see indirect calls, pointer aliasing, macro-named cells or reflection-like dispatch, and a local SHADOWING a cell name can be charged to the cell -- so every count is a FLOOR (counts_floor="1") and the blind spots are listed in the report's own legend.
- Every other indexed language is named on the root as unanalyzed_langs= and contributes no cells and no rows: that absence is NOT a measured zero.
### `--ensemble`
**Answers:** the FAMILY JOIN: per function, which of FOUR orthogonal evidence families fire, ranked by the COUNT of distinct families
**Try it**
_The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree._
```
$ ./build/ripwire . --ensemble --limit=8
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
### `--quality-panel[=PRESET]`
**Answers:** THE SINGLE COMMAND: the whole quality panel in ONE ranked report.
Per function, which of SIX evidence families fire -- structural (shape), lexical (identifier text), confusion (syntactic construct), historical (git churn), colocation (what you must read from outside this file), state (this function's OWN BODY touching non-local mutable state) -- ranked by the COUNT of distinct families, NEVER by a weighted composite, each row carrying its own evidence. PRESET selects and cuts, never weights: strict (the four families measured steady enough to gate on, 2 must agree) | default (all six, 2; the bare form) | lenient (all six, 1 -- a reading order, not a verdict). historical and colocation are out of strict: each is a fixed-size worst-40 cut over a ranking whose population moves, so both re-shuffle on code that did not change (docs/EVALS.md section 9.9). A family that could not be measured here is UNAVAILABLE, never 'did not fire', and of= drops with it. A lens: exit 0. Pages limit=N (offset=M). WHY NO COMPOSITE, in full (the emitted legend is deliberately terse and points here): averaging correlated metrics re-weights one signal and calls it six, and a single quotable number is wrong the moment it is quoted -- fam= is ORDINAL, and every row carries its own evidence so a reader can see WHY without a second command. The families are partitioned by KIND of evidence so that corroboration means the lenses failed DIFFERENTLY, not that one weakness echoed six times: the first four are the ensemble join, called through its own entry point and unchanged; colocation and state passed the same orthogonality test on the same corpora before being enabled. Every threshold is REUSED from the lens it came from, none is new: four absolute structural bars (cognitive complexity, lines, nesting, params), and three rankings with no defensible absolute cut, each firing for the worst decile of its OWN ranking (at least one row, at most that lens's default window of 40) -- an ordinal cut is RELATIVE, 'worst in THIS corpus', never 'bad in absolute terms'. The state family fires on the presence of a direct access site and deliberately uses the OWN-BODY half of the lens, not the callee closure: the panel's unit is one function's own comprehensibility, and the closure is a fact about its callees. UNAVAILABLE is never silent: an empty unavailable= means every family was measured, an empty ranking or empty language coverage counts as NOT measured, and the coverage denominators behind each verdict are published so it can be checked instead of trusted. The join=deep+untested annotation puts two facts already in the report side by side (sustained depth, no reaching test) because that pair is where a refactor is most wanted and least safe; counting it would be one family wearing a second hat.
**Try it**
_THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose._
```
$ ./build/ripwire . --quality-panel
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- PRESET selects and cuts, never weights: strict (the four families measured steady enough to gate on, 2 must agree) | default (all six, 2;
- the bare form) | lenient (all six, 1 -- a reading order, not a verdict).
- A family that could not be measured here is UNAVAILABLE, never 'did not fire', and of= drops with it.
### `--context-ratio`
**Answers:** the LOCAL-REASONING lens: to understand this symbol, how much must you know that is NOT in front of you? Per symbol (and rolled up per file) the distinct in-corpus definitions and files its reference sites resolve to, and the share of them defined OUTSIDE its own file — as an edge count (ent_ratio=) and, weighted by the tokens a reader must actually read, as read_ratio=.
ATTRIBUTION: the fraction itself is published — it is Beck and Diehl's per-class congruence (FSE 2011) flipped, with Martin's instability Ce/(Ca+Ce) as its crude ancestor. What is refined here is the READER WEIGHTING and the use of EVERY reference role (call, read, write, import, base class, member type), not calls alone. Resolution is NAME-BASED and language-gated, the same heuristic level the uses verb works at; a name with several definitions contributes each of them up to defs_per_name_cap= and amb= counts it. Names with no in-corpus definition land in ext=, which locals and parameters DOMINATE, so ext= is not a dependency count and is excluded from both ratios. ents=/files= are FLOORS. Pages with limit=N (offset=M); default 40 symbol rows and 40 file rows. An ORDERING, never a grade and never a threshold.
**Try it**
_The local-reasoning lens: to understand this symbol, how much must you know that is NOT in front of you (ent_ratio= edge share, read_ratio= token-weighted)._
```
$ ./build/ripwire . --context-ratio --limit=8
... [22 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- Resolution is NAME-BASED and language-gated, the same heuristic level the uses verb works at;
- Names with no in-corpus definition land in ext=, which locals and parameters DOMINATE, so ext= is not a dependency count and is excluded from both ratios.
- Pages with limit=N (offset=M);
### `--naming-calibration`
**Answers:** score the naming-* lint rules against this repo's OWN rename history: one git log pass mines old->new identifier substitutions, joins each to the symbol it became at HEAD, and scores BOTH spellings with the same predicates --lint runs.
old=fires on the abandoned spelling, new=fires on the chosen one, proxy=old/(old+new), where 0.50 is exactly chance. A NOISY PROXY, stated as one -- rebrands, moves and API changes all look like renames -- so read pairs= (the sample size) first; the group rules report scope=group-rule, not a fake 0/0. Exit 0 always: the per-rule floor lives in test/namingcalibrationcheck.sh
**Try it**
_Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first._
```
$ ./build/ripwire . --naming-calibration
... [17 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- A NOISY PROXY, stated as one -- rebrands, moves and API changes all look like renames -- so read pairs= (the sample size) first;
- the group rules report scope=group-rule, not a fake 0/0.
- Exit 0 always: the per-rule floor lives in test/namingcalibrationcheck.sh
### `--naming-consistency`
**Answers:** TIER A convention normalization (section 9.2): the corpus's OWN case-convention vote per (language, kind) group among multi-token eligible names -- a single-token name, or one split only on digit boundaries, carries no case signal and is silently excluded.
A group DECIDES only when its leading style (camel/pascal/snake/screaming) clears a 20-name sample floor AND a 90% agreement floor; short of either it reports style=UNAVAILABLE with why= naming which bar it missed, never a guessed winner. Every off-convention name in a DECIDED group (including mixed -- naming-case's own finding, a separator AND a transition in one name, which never wins a vote) gets propose=: its OWN subtokens mechanically recombined into the dominant style -- no dictionary, no synonym judgment, which is what keeps this derivable from the corpus rather than invented. propose= is a SUGGESTION, never a safe-to-blind-apply rename -- an actual rename needs --uses to prove the complete reference set first. Exit 0 always: a lens, not a gate. Pages limit=N (offset=M); default 40 rows
**Try it**
_The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename)._
```
$ ./build/ripwire . --naming-consistency --limit=8
... [24 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- A group DECIDES only when its leading style (camel/pascal/snake/screaming) clears a 20-name sample floor AND a 90% agreement floor;
- short of either it reports style=UNAVAILABLE with why= naming which bar it missed, never a guessed winner.
- propose= is a SUGGESTION, never a safe-to-blind-apply rename -- an actual rename needs --uses to prove the complete reference set first.
### `--naming-locals`
**Answers:** OPT-IN --lint MODIFIER (requires --lint;
refused alone), OFF by default: local-variable-indexing plan Phase 2 (docs/LOCALS_INDEXING.md). Runs the naming-short/naming-wordy/naming-underscore/naming-case predicates (same tags, same rule bodies as the existing Symbol-scoped checks) against LOCAL variable names too, C/C++ only, but ONLY inside a function that already clears an EXISTING size/complexity gate (loc>80 OR nest>4 OR ccx>=15 -- the shipped large-function/deep-nesting thresholds) AND has locals>=8 (measured floor: median locals=9 among this repo's own 377 gated functions) -- never a whole-corpus local-name sweep. naming-short additionally requires the local's own declDepth>=2 (nested, not the function's own outermost block). Deliberately breaks the lens's stated invariant that an un-indexed local can never be flagged -- read the WITHDRAWN note atop src/naminglens.h before relying on this. NOT default-enabled inside a plain --lint run and not a candidate for it yet: the plan's own hard blocker (a hand-curated fixture corpus AND a manual real-corpus audit for idiomatic-short-name skew -- i/j/k/buf/tmp/ err) has not run. Exit 0 always; findings ride the same naming-* tallies/floors as --lint
**Try it**
_The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate._
```
$ ./build/ripwire . --lint --naming-locals
... [17 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- refused alone), OFF by default: local-variable-indexing plan Phase 2 (docs/LOCALS_INDEXING.md).
- Deliberately breaks the lens's stated invariant that an un-indexed local can never be flagged -- read the WITHDRAWN note atop src/naminglens.h before relying on this.
- NOT default-enabled inside a plain --lint run and not a candidate for it yet: the plan's own hard blocker (a hand-curated fixture corpus AND a manual real-corpus audit for idiomatic-short-name skew -- i/j/k/buf/tmp/ err) has not run.
### `--comment-coherence`
**Answers:** per function/method WITH A DOC COMMENT, two published content measures, MOST NAME-RESTATING FIRST: c_coeff (Steidl/Hummel/Juergens, ICPC 2013) is the fraction of the comment's words within Levenshtein distance <2 of a word in the symbol's own (split) name — HIGH c_coeff IS BAD, it means the comment mostly repeats the name and adds no information (the opposite of the naive 'high coherence sounds good' reading).
cic (Scalabrino, ICPC 2016 / JSEP 2018) is the Jaccard overlap of two preprocessed term sets: the comment's vocabulary vs every identifier the definition's own span uses (operators/keywords stripped, camelCase/snake_case split, English stopwords dropped, deduplicated). The two measure different things and are expected to disagree — both are reported, never collapsed to one number. UNAVAILABLE (not scored, never a zero) where no doc comment exists, counted in no_comment= on the root. Complements --doc-drift (which checks whether a markdown CLAIM is stale) with comment CONTENT, over a disjoint input — neither verb duplicates the other. Pages with limit=N (offset=M); default 40 rows.
**Try it**
_Functions WITH a doc comment, most name-restating first: c_coeff (high = the comment repeats the name) and cic (Jaccard of comment vs identifier vocabulary), both reported, never collapsed._
```
$ ./build/ripwire . --comment-coherence --limit=8
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- The two measure different things and are expected to disagree — both are reported, never collapsed to one number.
- UNAVAILABLE (not scored, never a zero) where no doc comment exists, counted in no_comment= on the root.
- Pages with limit=N (offset=M);
### `--cochange[=FILE]`
**Answers:** files that change together in git (hidden coupling;
the rows' own legend defines surprising=)
**Try it**
_Files that change together in git (hidden coupling)._
```
$ ./build/ripwire . --cochange
... [21 more line(s); run it to see the whole thing]
```
**Shaped by:** `--cochange-recur`, `--cochange-groups`, `--since`, `--limit`
### `--cochange-recur=K`
**Answers:** (with --cochange) report only pairs whose co-change RECURS in K or more of the mined window's sub-windows, so a one-off refactor sprint stops reading like an eighteen-month structural defect (Clio, ICSE 2011).
Every row carries recur= with or without this flag; the header publishes sub_windows= (the denominator) and min_recur= when the filter is on
**Try it**
_Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect._
```
$ ./build/ripwire . --cochange --cochange-recur=2
... [21 more line(s); run it to see the whole thing]
```
### `--cochange-groups`
**Answers:** (with --cochange, repo-wide only) emit Modularity Violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" is ONE row that names the file to fix (Mo/Cai/Kazman, IEEE TSE 2019).
A greedy cover, disclosed as greedy — set cover is NP-hard, so the group count is an upper bound on the minimum, not the minimum
**Try it**
_Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy._
```
$ ./build/ripwire . --cochange --cochange-groups
... [17 more line(s); run it to see the whole thing]
```
### `--since=REV|DATE`
**Answers:** scope --hotspots/--cochange/--rank-by=churn|churn-decay to commits after this point: a revision (HEAD~20, a tag/sha — deterministic) or a git approxidate ("2 weeks ago" — wall-clock-relative).
e.g. --hotspots --since="1 week ago" ranks by RECENT churn (the regression lens). Absent ⇒ each verb's OWN bounded default window, NOT all history: --hotspots 12 months, --rank-by=churn 18 months, --cochange 18 months (--rank-by=churn-decay is the ONE exception: its default IS all history, because the 90-day half-life makes a cut-off unnecessary — it stamps that too). All of them STAMP the window they used AND the anchor that produced it: a DEFAULT window is measured back from HEAD's OWN committer date, never the wall clock, and says so (window="12mo@HEAD"/"18mo@HEAD"), so a pinned corpus, an archived release or a bisect checkout has its history INSIDE its own window — unanchored, a 2024 checkout read in 2026 reported no repository at all. A value you pass HERE is never re-anchored: it is stamped verbatim, so --since="18 months ago" is the wall-clock window on request. An UNRESOLVABLE value is refused by --hotspots (exit 1 — its window is part of the measurement) and degrades to the verb's own default window elsewhere BESIDE --slice=SYM:VAR it is not a window at all: it names the revision whose def-use slice of that variable this run is diffed against — see --slice
**Try it**
_Hotspots scoped to RECENT churn (the regression lens)._
```
$ ./build/ripwire . --hotspots --since="2 weeks ago"
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--slice`
**Caveats (stated by the binary):**
- Absent ⇒ each verb's OWN bounded default window, NOT all history: --hotspots 12 months, --rank-by=churn 18 months, --cochange 18 months (--rank-by=churn-decay is the ONE exception: its default IS all history, because the 90-day half-life makes a cut-off unnecessary — it stamps that too).
- A value you pass HERE is never re-anchored: it is stamped verbatim, so --since="18 months ago" is the wall-clock window on request.
- An UNRESOLVABLE value is refused by --hotspots (exit 1 — its window is part of the measurement) and degrades to the verb's own default window elsewhere BESIDE --slice=SYM:VAR it is not a window at all: it names the revision whose def-use slice of that variable this run is diffed against — see --slice
### `--arch=FILE`
**Answers:** enforce layering rules (exit 2 on violation);
the Martin Ca/Ce/I/A/D block it emits is a design heuristic, not independently outcome-validated (never gates). propagation_cost's N is dependency-capable files only, same denominator as --deps . Layer substrings and regex path-rules match the ROOT-RELATIVE path (src/core/x.cpp), not the spelling you passed, so a rules file means the same thing in every checkout
**Try it**
_Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules._
```
$ ./build/ripwire . --arch=test/archfix/rules.txt
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--graph-query`, `--deps`
**Caveats (stated by the binary):**
- the Martin Ca/Ce/I/A/D block it emits is a design heuristic, not independently outcome-validated (never gates).
### `--arch=FILE --baseline`
**Answers:** write .ripwire_arch_baseline (accept current debt as baseline), exit 0
**Try it**
_Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules._
```
$ ./build/ripwire . --arch=test/archfix/rules.txt
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--graph-query`, `--deps`
### `--arch=FILE --baseline-update`
**Answers:** merge current violations into baseline (accept new debt), exit 0
**Try it**
_Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules._
```
$ ./build/ripwire . --arch=test/archfix/rules.txt
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--graph-query`, `--deps`
### `--lint`
**Answers:** built-in AST checks (c-cast, goto, unsafe-c-fn, naming-*, cache-* data-layout, ...).
naming-uninformative is ONE-SIDED by design: it fires only when a name's subtokens are ALL corpus-common (BM25 idf over the identifier-name corpus) AND its body clears a size floor — a high-idf (distinctive) name is never penalised, unlike the withdrawn name<->body rule. Each row's applicability is per-LANGUAGE, not per-file-content: a rule whose registered languages (see --lint-catalog) intersect NONE of the corpus' languages carries applicable="0" (its count="0" is then structural inertness, not a measurement), and the root tallies inert_rules="N"; see --lint-catalog for the full registry
**Try it**
_Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._
```
$ ./build/ripwire . --lint
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--expand`, `--naming-calibration`, `--naming-locals`, `--lint-catalog`, `--lint-rules`, `--lint-select`, `--lint-ignore`
**Caveats (stated by the binary):**
- naming-uninformative is ONE-SIDED by design: it fires only when a name's subtokens are ALL corpus-common (BM25 idf over the identifier-name corpus) AND its body clears a size floor — a high-idf (distinctive) name is never penalised, unlike the withdrawn name<->body rule.
- Each row's applicability is per-LANGUAGE, not per-file-content: a rule whose registered languages (see --lint-catalog) intersect NONE of the corpus' languages carries applicable="0" (its count="0" is then structural inertness, not a measurement), and the root tallies inert_rules="N";
### `--lint-catalog`
**Answers:** the built-in rule registry: one row per rule with sev=/category=/rationale/lang=/since= — no corpus needed.
Every built-in rule from every pack (base checks, atoms-*, cache-*, naming-*, the symbol-level checks) has exactly one row; lang= is the SAME token spelling --lint-rules' language: field accepts, so it round-trips into a user rule
**Try it**
_The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed._
```
$ ./build/ripwire . --lint-catalog
a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_casta goto statement — cppcoreguidelines-avoid-gotoa do/while loop shape (on Swift, its do/catch block shares the same grammar node and also matches)a call to an unbounded C string function (strcpy/strcat/sprintf/gets)a call to a broken hash or cipher (MD5/SHA1/MD4/RC4)a doubly-parenthesized expression — readability-redundant-parenthesesan if-body that is just `;` — bugprone-suspicious-semicolona C-style typedef struct/union where `using` is preferreda non-trivial numeric literal inside a function body, outside a const/constexpr init
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--lint`
### `--lint-rules=DIR`
**Answers:** load user lint rules (YAML, ast-grep style) from DIR — runs with, or instead of, --lint
**Try it**
_User lint rules (YAML, ast-grep style) from a directory._
```
$ ./build/ripwire . --lint-rules=test/lintrulesfix/rules
use LOG() instead of printfuse LOG() instead of printfuse LOG() instead of printfuse LOG() instead of printfuse LOG() instead of printf
```
**Shaped by:** `--lint-catalog`, `--lint-select`, `--lint-ignore`, `--sarif`
### `--lint-select=PREFIX[,...]`
**Answers:** (with --lint / --lint-rules) run ONLY rules whose name starts with one of these PREFIXes (or '*' for all) — comma-separated, e.g.
cache- selects the whole cache-* family. The root then carries selected="K of N" plus the raw select=/ignore= you passed, so a filtered zero is never confusable with an unfiltered one. An unresolvable PREFIX (matches no rule) refuses (exit 1), naming the nearest rule/family by edit distance
**Try it**
_An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-)._
```
$ ./build/ripwire . --lint --lint-select=cach-
(empty)
```
**Shaped by:** `--lint-ignore`
**Caveats (stated by the binary):**
- The root then carries selected="K of N" plus the raw select=/ignore= you passed, so a filtered zero is never confusable with an unfiltered one.
- An unresolvable PREFIX (matches no rule) refuses (exit 1), naming the nearest rule/family by edit distance
### `--lint-ignore=PREFIX[,...]`
**Answers:** (with --lint / --lint-rules) DROP rules whose name starts with one of these PREFIXes (or '*' to drop everything, e.g.
paired with --lint-select elsewhere to isolate one family) — applied AFTER --lint-select narrows the set; same unresolvable-PREFIX refusal and root disclosure as --lint-select
**Try it**
_DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root._
```
$ ./build/ripwire . --lint --lint-ignore=naming-,cache-
... [17 more line(s); run it to see the whole thing]
```
### `--sarif`
**Answers:** (with --lint / --lint-rules) the SAME findings as SARIF 2.1.0 instead of the native XML block — the shape github/codeql-action/upload-sarif consumes for code scanning.
Pure re-serialization (zero new analysis); results count == the native run's findings count. Levels: user severity error/warn/info -> SARIF error/warning/note; a built-in finding (a fact, never a gate) has no severity of its own and also maps to note. Fields with no SARIF home (per-rule capped= floor, enclosing symbol, raw sev=) ride in properties rather than being dropped; URIs are relative to the scanned root. Always the FULL result set — refuses loudly alongside limit=/offset= paging, --match and --with-profile
**Try it**
_The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's._
```
$ ./build/ripwire . --lint --sarif
{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line]
{"id":"goto","shortDescription":{"text":"goto"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"do-while","shortDescription":{"text":"do-while"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"unsafe-c-fn","shortDescription":{"text":"unsafe-c-fn"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"weak-crypto","shortDescription":{"text":"weak-crypto"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"redundant-parens","shortDescription":{"text":"redundant-parens"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"suspicious-semicolon","shortDescription":{"text":"suspicious-semicolon"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"typedef-over-using","shortDescription":{"text":"typedef-over-using"},"properties":{"builtin":true,"capped":false,"applicable":true}},
{"id":"magic-number","shortDescription":{"text":"magic-number"},"properties":{"builtin":true,"capped":true,"applicable":true}},
{"id":"empty-catch","shortDescription":{"text":"empty-catch"},"properties":{"builtin":true,"capped":false,"applicable":true}},
... [21 more line(s); run it to see the whole thing]
```
**Shaped by:** `--legend`
**Caveats (stated by the binary):**
- a built-in finding (a fact, never a gate) has no severity of its own and also maps to note.
- Fields with no SARIF home (per-rule capped= floor, enclosing symbol, raw sev=) ride in properties rather than being dropped;
- Always the FULL result set — refuses loudly alongside limit=/offset= paging, --match and --with-profile
### `--with-profile=FILE`
**Answers:** (with --lint) join MEASURED heat onto findings: FILE is a RIPWIRE_PROFILE build's report (its #PROF_TSV block);
a finding whose enclosing symbol contains a PROFILE_SCOPE site gains heat_* attributes (scope, calls, total_ms, and whichever counter columns the profiled run armed — l1d_mpki etc.). Static shape x measured weight; joins nothing silently — a missing file or a FILE with no #PROF_TSV block refuses loudly
**Try it**
_Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row._
```
$ ./build/ripwire . --lint --with-profile=report.txt
#PROF_TSV_BEGIN one row per scope, aggregated across threads; counters are RAW integers
scope file line calls total_ms l1d_mpki
walk: chase pass x.cpp 9 12 48.500 7.250
#PROF_TSV_END
```
**Shaped by:** `--sarif`
**Caveats (stated by the binary):**
- joins nothing silently — a missing file or a FILE with no #PROF_TSV block refuses loudly
### `--communities`
**Answers:** cluster the call graph into cohesive modules (each row's id= drills down below;
drill= names the verb)
**Try it**
_Cluster the call graph into cohesive modules._
```
$ ./build/ripwire . --communities
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--community`, `--limit`
### `--community=ID`
**Answers:** ONE module from that partition: its FULL ranked member list (40 rows by default, raise with --limit, page with --offset) plus its bridge edges to every other module it touches.
ID is an id= from --communities/--zoom; ids live in 0..partition-1 (the child's partition= — the full label space, isolated singletons included), so a single-member module is a legal drill-down and reports size="1". modules= counts the non-isolated communities (same number as the parent's modules=). An id outside 0..partition-1 REFUSES, naming the valid range and the nearest legal id -- a bad id is a typo, not an empty module
**Try it**
_Drill into ONE call-graph community by id — the drill= the --communities output itself advertises._
```
$ ./build/ripwire . --community=0
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- ONE module from that partition: its FULL ranked member list (40 rows by default, raise with --limit, page with --offset) plus its bridge edges to every other module it touches.
- An id outside 0..partition-1 REFUSES, naming the valid range and the nearest legal id -- a bad id is a typo, not an empty module
### `--zoom[=depth]`
**Answers:** NESTED module hierarchy (multi-level Louvain) + cross-module bridges;
--zoom --mermaid = nested diagram. Default window: the top 2 levels (levels_shown=; a module at the cut carries children= for its unprinted child modules; --zoom-levels=N prints N, 0 = all) over the 40 largest top modules (shown=/capped=/next_offset=, next= pastes the next page; --limit=N/--offset=M window them)
**Try it**
_Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=._
```
$ ./build/ripwire . --zoom
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--community`, `--limit`
**Caveats (stated by the binary):**
- --zoom-levels=N prints N, 0 = all) over the 40 largest top modules (shown=/capped=/next_offset=, next= pastes the next page;
- --limit=N/--offset=M window them)
### `--report`
**Answers:** architecture summary (modules, god-files, cycles) as markdown
**Try it**
_Architecture summary (modules, god-files, cycles) as markdown._
```
$ ./build/ripwire . --report
# ripwire architecture report
1616 files · 14206 symbols · 17404 edges · 1098 modules (8360 call-graph isolated)
Root: `.`
Call-graph isolate provenance: 2106 declaration, 1035 header, 2769 source, 2450 document; 0 connected Louvain singletons
## Modules (call-graph clusters; showing 12 of 1098)
- **src::min@infra/fastmath.h:51:2347 [run,write,emit]** — 470 symbols
- **src::assign@infra/svector.h:342:19905 [compute,resolve,find]** — 439 symbols
- **src::VERIFY@infra/Diagnostics.h:172:8901 [compute,apply,find]** — 394 symbols
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--legend`, `--limit`
### `--seams`
**Answers:** cross-module call seams no test reaches (untested integration seams)
**Try it**
_Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._
```
$ ./build/ripwire . --seams
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--limit`
### `--mermaid`
**Answers:** module (directory) dependency graph as a Mermaid diagram (paste/render)
**Try it**
_Module (directory) dependency graph as a Mermaid diagram._
```
$ ./build/ripwire . --mermaid
%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live.
flowchart LR
subgraph sg0 ["src"]
n85["src 4703"]
n86["src/infra 498"]
end
subgraph sg1 ["test"]
n87["test 2801"]
n165["test/expandmodefix 151"]
n212["test/massfix 77"]
n201["test/legofix 60"]
n232["test/optremarksfix 59"]
n271["test/sliceflowsensfix 59"]
n244["test/pyshapefix 58"]
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--zoom`, `--with-graph`, `--legend`, `--limit`
### `--owners[=SYM]`
**Answers:** bus-factor: recency-weighted author ownership per file;
bf=1 = one person holds >80% of weighted commits. Files with authors=1 (deterministically bf=1 share=1.00) fold into ONE summary row instead of N identical rows; --detail=N restores the full listing. An @FILE:LINE seed rebinds to the innermost enclosing definition (sym= names it) and analyses exactly that definition's file
**Try it**
_Bus-factor: recency-weighted author ownership per file._
```
$ ./build/ripwire . --owners
... [12 more line(s); run it to see the whole thing]
```
**Shaped by:** `--at`, `--limit`
### `--dead-code[=DIR]`
**Answers:** high-confidence internal source functions with no caller in the indexed tree;
=DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed. A symbol whose definition is produced by a SELF-REGISTERING test/benchmark macro is never reported: doctest TEST_CASE/ TEST_CASE_FIXTURE/SCENARIO, gtest TEST/TEST_F/TEST_P, Catch2, Google Benchmark — a static initializer registers them, so a name-based call graph cannot see the caller and every one of them would be a false positive. Extend the list for your own framework with `.ripwire_config`'s one key, `register_macros = NAME[, NAME...]` (one directive per line, # comments). The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0. Exempt from dead-code only — such a symbol still participates in clone detection. A LEADING ./ anchors DIR at the repo ROOT (=./src matches only the top-level src/ subtree); a bare name (=src) matches that component ANYWHERE in the tree, including nested (test/fixture/src/…)
**Try it**
_High-confidence internal functions with no caller. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory._
```
$ ./build/ripwire . --dead-code=src
```
**Shaped by:** `--safe-delete`, `--limit`
**Caveats (stated by the binary):**
- =DIR scopes to whole path components (dir or filename) and REFUSES a filter that names nothing indexed.
- The exemption is DISCLOSED, never silent: register-macro-excluded="N" rides the report and prints even at 0.
### `--quality-baseline`
**Answers:** snapshot ccx/clones/dead-code to .ripwire_quality_baseline (run BEFORE a change, on a CLEAN tree).
On a tree that DIFFERS from HEAD it computes the HEAD delta FIRST and REFUSES (exit 1) rather than pin the debt already in the tree as the floor — it names how many gating findings it would absorb and the first of them. Commit, or pass --allow-dirty.
**Try it**
_REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward._
```
$ ./build/ripwire . --quality-baseline
(empty)
```
**Shaped by:** `--allow-dirty`, `--quality-delta`, `--legend`
**Caveats (stated by the binary):**
- On a tree that DIFFERS from HEAD it computes the HEAD delta FIRST and REFUSES (exit 1) rather than pin the debt already in the tree as the floor — it names how many gating findings it would absorb and the first of them.
### `--allow-dirty`
**Answers:** (with --quality-baseline) pin anyway: the sidecar is stamped with the dirty pin and the absorbed count, and every later --quality-delta against it carries baseline_absorbed="N" — so a green exit beside that attribute reads as "clean SINCE THE PIN", never "clean".
Refused alone.
**Try it**
_The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it._
```
$ ./build/ripwire . --quality-baseline --allow-dirty
(empty)
```
**Shaped by:** `--quality-baseline`
**Caveats (stated by the binary):**
- (with --quality-baseline) pin anyway: the sidecar is stamped with the dirty pin and the absorbed count, and every later --quality-delta against it carries baseline_absorbed="N" — so a green exit beside that attribute reads as "clean SINCE THE PIN", never "clean".
### `--quality-delta`
**Answers:** agent self-check before a PR (pair with --test-gate): report ONLY what a change made worse vs the baseline (10 kinds: complexity/verbosity/nesting/params/dup/dead/api-surface + error-masking/short-horizon-churn/reuse-decline);
every finding is classified by ORIGIN: a symbol that EXISTED at the baseline and got worse (preexisting-worse="N", no attribute on the row) vs one that exists only because the code is NEW (new-symbol="N", origin="new-symbol" on the row). A small numeric delta is additionally sev="minor". EXIT 2 ONLY on preexisting-worse AND major AND unacked — the gating="N" header count. New-symbol rows are still PRINTED (they are the debt you are adding — read them), they just never gate; exit 0 means "nothing that already existed got worse", not "clean". Clone kinds classify by member set (new-symbol only if EVERY member is new); short-horizon-churn is preexisting by construction. LIMIT: origin is canonId (path::scope::name) identity, so a RENAMED/MOVED symbol reads as new and a regression carried in with the move will not gate. Test-fixture dirs + doc sections are exempt from dead-code/churn; churn needs COMMITTED thrash evidence (rewritten across recent commits AND again by this diff), never the current edit alone WHICH FLOOR IT COMPARES AGAINST, and a side effect: the sidecar is honored only when the sha it was pinned at EQUALS the current git HEAD (strict equality — an ancestor commit describes a DIFFERENT tree, so everything committed since would read as your regression). A sidecar pinned anywhere else is STALE: this verb then DELETES it from your working tree (self-heal, so the next run does not rediscover the dead pin) and auto-compares the working tree vs git HEAD instead. Re-pin with --quality-baseline. The read-only MCP quality_delta verb applies the SAME staleness test but never deletes. Which floor was actually used is on every report as baseline=: sidecar | git-HEAD | git-HEAD (stale sidecar removed) | git-HEAD (stale sidecar ignored) — the last two say a stale sidecar existed, and 'removed' means the file is gone. A non-git root has no HEAD to fall back to, so its sidecar is always honored; without one there, the verb exits 1.
**Try it**
_Recorded against a DIRTY tree, so any row below is a real regression in the working copy. The sandbox section below shows the same gating shape on a known, deliberate edit._
```
$ ./build/ripwire . --quality-delta
... [10 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--dmm`, `--quality-ack`, `--scope`, `--json`
**Caveats (stated by the binary):**
- New-symbol rows are still PRINTED (they are the debt you are adding — read them), they just never gate;
- LIMIT: origin is canonId (path::scope::name) identity, so a RENAMED/MOVED symbol reads as new and a regression carried in with the move will not gate.
- The read-only MCP quality_delta verb applies the SAME staleness test but never deletes.
### `--quality-delta=REV|A..B`
**Answers:** the same 10-kind report between two COMMITTED TREES instead of the working tree vs a baseline — the WAVE-level measurement (=A..B = tree B against tree A;
=REV = that commit against its FIRST PARENT; an EMPTY side of the range means HEAD). Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B. Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced. Identical output contract to the bare form — same kinds, gating="N", exit 2, and the same .ripwire_quality_acks ratchet (acks are keyed root-relative, so a ledger recorded from working-tree runs applies unchanged). base_ref= and target_ref= disclose the two RESOLVED shas. No sidecar is read, written or deleted by this form, and at= is omitted: the two refs ARE the anchor. A==B is a legal, empty, exit-0 comparison. ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs. The other 9 kinds are computed exactly as the bare form computes them.
**Try it**
_Recorded against a DIRTY tree, so any row below is a real regression in the working copy. The sandbox section below shows the same gating shape on a known, deliberate edit._
```
$ ./build/ripwire . --quality-delta
... [10 more line(s); run it to see the whole thing]
```
**Shaped by:** `--affected`, `--test-gate`, `--allow-dirty`, `--dmm`, `--quality-ack`, `--scope`, `--json`
**Caveats (stated by the binary):**
- Same grammar --dmm= takes, and A...B is REFUSED rather than read as A..B.
- Use it to measure a whole integration branch at once (--quality-delta=..): per-lane checks each compare against their own baseline and cannot see a regression the WAVE introduced.
- ONE KIND CANNOT BE MEASURED HERE and says so as churn="unavailable": short-horizon-churn needs git history at the tree being judged, and both trees are materialized OUT of the repo into temp dirs.
### `--dmm[=REV|A..B]`
**Answers:** the DELTA MAINTAINABILITY MODEL scalar: ONE comparable number in [0,1] for a change, so quality becomes TRENDABLE across commits instead of a per-kind list (di Biase, Rastogi, Bruntink and van Deursen, TechDebt 2019;
thresholds and arithmetic from PyDriller's deltamaintainability reference implementation). Bare = the WORKING TREE vs git HEAD (what --quality-delta compares); =REV = that commit vs its FIRST PARENT (the per-commit scalar); =A..B = tree B vs tree A. A UNIT is a function/method definition with a body; its VOLUME is its line span. Per property a unit is LOW risk iff size: loc<=15, complexity: cyclomatic<=5, interfacing: params<=2. good = low-risk volume ADDED plus high-risk volume REMOVED; bad = low-risk REMOVED plus high-risk ADDED; dmm = good/(good+bad). So DELETING a god function scores 1.000 and GROWING one scores 0.000. The three sub-scores (size/complexity/interfacing) are emitted alongside the combined one because they are separately actionable; the combined one POOLS them (summed good over summed good+bad) and is labelled combine="pooled", since the paper publishes the three separately and no aggregate. IT IS A DELTA, NEVER A LEVEL: a unit you edit without changing its size, complexity or parameter count sits in the same bin with the same volume on both sides and contributes NOTHING. Touching bad code is not punished, deliberately, because a gate that punishes it is a gate people route around. dmm="UNAVAILABLE" means good+bad was 0 (a rename, a literal edit, a comment reflow): the change is outside what the model measures. That is NEVER to be read as 1.000 or 0.000, and reason= says which case it was. Same token per property. VOLUME IS PHYSICAL LINE SPAN (size_metric="physical-loc"), where the reference implementation uses non-comment non-blank lines, so a heavily commented unit crosses the size threshold here earlier. NO THRESHOLD, NO VERDICT, ALWAYS EXIT 0.
**Try it**
_The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a DIRTY tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1._
```
$ ./build/ripwire . --dmm
```
**Shaped by:** `--quality-delta`
**Caveats (stated by the binary):**
- IT IS A DELTA, NEVER A LEVEL: a unit you edit without changing its size, complexity or parameter count sits in the same bin with the same volume on both sides and contributes NOTHING.
- That is NEVER to be read as 1.000 or 0.000, and reason= says which case it was.
### `--quality-ack[=REASON]`
**Answers:** accept the current findings into .ripwire_quality_acks (per-finding ratchet): re-runs suppress them honestly (acked="N") until one WORSENS past its acked size.
=REASON implies the --quality-delta report it acks; the reason-less spelling needs --quality-delta beside it (refused alone). An ack with 0 findings to accept writes nothing and says so.
**Try it**
_NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything._
```
$ ./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope
(empty)
```
**Shaped by:** `--ack-only`, `--scope`, `--legend`
**Caveats (stated by the binary):**
- the reason-less spelling needs --quality-delta beside it (refused alone).
### `--ack-only=SUBSTR[,SUBSTR]`
**Answers:** (with --quality-ack) ack only SOME findings — those whose KIND, canonical id, or FACET contains one of these;
the pseudo-token 'gating' selects exactly what would exit 2. Bare --quality-ack accepts the WHOLE report, so accepting one deliberate change silently accepts the rest — how a ratchet turns into a rubber stamp. Prefer the facet: --ack-only=contract-change acks the deliberate arity changes WITHOUT the never-gating api-surface new-symbol rows. Matching nothing refuses (exit 1) rather than falling back to acking everything. Whatever you leave unacked stays visible.
**Try it**
_--ack-only WITHOUT --quality-ack REFUSES loudly (exit 1, the pairing named) — it used to be silently ignored._
```
$ ./build/ripwire . --ack-only=gating
(empty)
```
**Shaped by:** `--scope`
**Caveats (stated by the binary):**
- Prefer the facet: --ack-only=contract-change acks the deliberate arity changes WITHOUT the never-gating api-surface new-symbol rows.
- Matching nothing refuses (exit 1) rather than falling back to acking everything.
### `--scope=GLOB[,GLOB...]`
**Answers:** (with --quality-delta/--quality-ack) OWNERSHIP partition for a working tree that has MORE THAN ONE WRITER in it — N agent sessions sharing one checkout.
The delta compares the working tree against HEAD, so every concurrent writer's uncommitted rows land in YOUR report; this files each finding by its p= path. Rows in scope gate as usual; rows outside it are STILL PRINTED, under an out-of-scope element with a do-not-ack banner, and never gate. The header carries scope=, scoped-out= and scoped-out-gating= (how many disclosed rows WOULD have gated — do not read a green exit as a clean tree). THE POINT IS THE ACK: bare --quality-ack in a dirty shared tree accepts the WHOLE report, which silently absorbs a sibling session's debt into a committed ledger under your reason string — that is how a ratchet becomes a rubber stamp. Under --scope, an out-of-scope row is never written, and an --ack-only that NAMES one refuses (exit 1, naming the rows, writing nothing). Each row written under a scope records by=, and a later run flags an ack whose by= does not cover what it suppresses (foreign-acks= plus an sa row with why="foreign-scope"). THE GLOB, EXACTLY (a pattern that silently fails to match is worse than a documented prefix): each comma-separated pattern is matched against the ROOT-RELATIVE path p= prints, and the list is an OR. NO wildcard = a ROOT-ANCHORED path prefix ending on a / boundary (scope=alpha matches alpha/lib.h, never alphabet/lib.h and never a nested src/alpha/ — stricter than the dead-code directory filter, on purpose). With * or ? = matched against the WHOLE path, * spanning / and ? exactly one character. NOT SUPPORTED: ** (it is two stars, and one already spans /), character classes, brace expansion, negation; whitespace and XML metacharacters in a pattern are REFUSED, not mangled. FLOORS: a clone group is in scope iff ANY member matches; a finding with no locator at all is filed OUT of scope (not provably yours); a scope naming nothing indexed REFUSES (exit 1) rather than reporting a clean zero. ONE RESERVED WORD: --scope=diff is the files the WORKING TREE changes vs the baseline, expanded to one path per changed INDEXED file (the count travels with the report as scope-diff-files=). It composes by UNION: --scope=diff,src/quality.h is that set plus that file. A directory really called diff must be spelled ./diff or diff/. IT IS SUGAR FOR THE SINGLE-WRITER CASE and wrong on its own in the shared tree this flag exists for — a sibling's edits are "changed" too, so name your own paths when the tree has more than one writer. Refused, never silently widened, when there is no git, when the range form is in play (it compares two COMMITTED trees), or when it expands to nothing. An ack written under it records by=diff, which a later run does NOT sweep: an auto-scope meant one file set then and another now, so re-checking it would invent findings.
**Try it**
_OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have._
```
$ ./build/ripwire . --quality-delta --scope=src/graph.h
... [25 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- rows outside it are STILL PRINTED, under an out-of-scope element with a do-not-ack banner, and never gate.
- Under --scope, an out-of-scope row is never written, and an --ack-only that NAMES one refuses (exit 1, naming the rows, writing nothing).
- NO wildcard = a ROOT-ANCHORED path prefix ending on a / boundary (scope=alpha matches alpha/lib.h, never alphabet/lib.h and never a nested src/alpha/ — stricter than the dead-code directory filter, on purpose).
### `--edit-check=SYM`
**Answers:** fast per-symbol post-edit contract check: SYM's param count + publicness NOW vs git HEAD (unchanged/new-symbol/contract-change with was/now), plus its 1-hop callers with any call-site provably incompatible with the NEW arity flagged.
A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES (exit 1) and lists the file:name spellings that pick one — unlike --callers/--uses, this verb may not union overloads and disclose defs=. A .ripwire_notes entry targeting SYM (or its file) rides along as a child, the same row shape --for/--expand surface. PRE-APPLY PREVIEW — add --edit-payload=FILE|- --dry-run to ask the SAME question about bytes that have NOT been written yet. The payload is spliced over SYM's definition span in memory, exactly as --replace-symbol-body would write it; that one file is re-parsed, the call graph rebuilt over the re-derived tree, and the same document emitted with preview="1" plus an child holding the CURRENT span the apply would replace, as on disk (over 4 KB: the head, with shown=/capped="1"/elided_lines=) — preview then apply, no Read. Nothing is written, and every other file plus the git HEAD baseline stay the real tree's. Refuses, exit 1, on a payload that is unreadable, empty, oversize or NUL-bearing, on one whose splice raises the file's parse errors, on one that does not define SYM, and on a span the file's current bytes no longer fit. Single-root, and it previews a body REPLACEMENT only.
**Try it**
_Fast per-symbol post-edit contract check vs git HEAD — recorded against a DIRTY tree, so the verdict describes the working copy, not HEAD alone._
```
$ ./build/ripwire . --edit-check=rankGraphTeleport
```
**Shaped by:** `--impact`, `--edit-target-file`, `--slice`, `--at`, `--legend`
**Caveats (stated by the binary):**
- A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES (exit 1) and lists the file:name spellings that pick one — unlike --callers/--uses, this verb may not union overloads and disclose defs=.
- that one file is re-parsed, the call graph rebuilt over the re-derived tree, and the same document emitted with preview="1" plus an child holding the CURRENT span the apply would replace, as on disk (over 4 KB: the head, with shown=/capped="1"/elided_lines=) — preview then apply, no Read.
- Refuses, exit 1, on a payload that is unreadable, empty, oversize or NUL-bearing, on one whose splice raises the file's parse errors, on one that does not define SYM, and on a span the file's current bytes no longer fit.
### `--replace-symbol-body=TARGET`
**Answers:** atomically replace one uniquely-resolved definition with the bytes from --edit-payload=FILE|- (ONE trailing newline on the payload folds into the newline already after the span — a heredoc or echo always appends one the span never had;
disclosed as trailing_newline_folded=true; a second one, a deliberate blank line, stays)
**Try it**
_An unknown TARGET refuses and leaves every file byte-identical._
```
$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h
(empty)
```
**Shaped by:** `--edit-check`, `--edit-target-file`, `--at`
**Caveats (stated by the binary):**
- atomically replace one uniquely-resolved definition with the bytes from --edit-payload=FILE|- (ONE trailing newline on the payload folds into the newline already after the span — a heredoc or echo always appends one the span never had;
### `--insert-before-symbol=TARGET`
**Answers:** atomically insert the payload immediately before one uniquely-resolved definition
**Try it**
_Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._
```
$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h
{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":8589,"new_file_bytes":8677,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"sep … [line truncated: 742 more bytes on this line]
{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]},
{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]},
{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}],
"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true},"tests_to_run":[{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","run":"bash test/adaptivecutshapecheck.sh"},
{"p":"test/verify_csr.cpp","run":"bash test/a9disclosurecheck.sh"},
{"p":"test/verify_radix.cpp","run":"bash test/greptiercheck.sh"}],
"tests":3,"script_gates_unmodelled":581,"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"}
```
### `--insert-after-symbol=TARGET`
**Answers:** atomically insert the payload immediately after one uniquely-resolved definition.
Both inserts PAD the block (newlines only, never removed) so it is separated from the anchor by the same blank-line run the file already uses at that seam; separator_padded=N on the receipt is the count added (0 when the payload carried its own). A payload carrying MORE …[REDACTED:kind] markers than the bytes it would replace already do (a body copied from a redacted serve) refuses: re-fetch that body with --no-redact. TARGET is a symbol name, an @FILE:LINE line-seed (edits the innermost definition enclosing that line — paste the location from a diff hunk or error; the receipt discloses resolved_from_seed, a faulted seed refuses with a specific diagnosis, and --edit-target-file may not accompany a seed), or a freshness-pinned sym# handle emitted by --grep --handles.
**Try it**
_Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call._
```
$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h
{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":8413,"new_file_bytes":8501,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator_p … [line truncated: 639 more bytes on this line]
"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true},"tests_to_run":[{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","run":"bash test/adaptivecutshapecheck.sh"},
{"p":"test/verify_csr.cpp","run":"bash test/a9disclosurecheck.sh"},
{"p":"test/verify_radix.cpp","run":"bash test/greptiercheck.sh"}],
"tests":3,"script_gates_unmodelled":581,"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true,"next":"bash test/adaptivecutshapecheck.sh"}
```
**Caveats (stated by the binary):**
- Both inserts PAD the block (newlines only, never removed) so it is separated from the anchor by the same blank-line run the file already uses at that seam;
- A payload carrying MORE …[REDACTED:kind] markers than the bytes it would replace already do (a body copied from a redacted serve) refuses: re-fetch that body with --no-redact.
- the receipt discloses resolved_from_seed, a faulted seed refuses with a specific diagnosis, and --edit-target-file may not accompany a seed), or a freshness-pinned sym# handle emitted by --grep --handles.
### `--edit-payload=FILE|-`
**Answers:** required exact byte payload ('-' reads stdin);
empty payloads refuse, never imply deletion
**Try it**
_An unknown TARGET refuses and leaves every file byte-identical._
```
$ ./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h
(empty)
```
**Shaped by:** `--edit-check`, `--replace-symbol-body`
**Caveats (stated by the binary):**
- empty payloads refuse, never imply deletion
### `--edit-target-file=PATH`
**Answers:** optional file-path substring disambiguating a same-named definition.
RELATIVE (matched against the indexed spelling) or ABSOLUTE (matched against the file's resolved on-disk path), so the path a receipt or a trace hands you works verbatim. These three CLI verbs reuse the MCP edit engine: freshness hash, lock, pre-rename recheck, fsync, mode preservation and atomic rename. Every refusal leaves the target byte-identical. Success prints a JSON receipt whose span is the POST-EDIT byte range (where the payload now sits in the new file), NOT the region overwritten in the old one — for --replace-symbol-body those two lengths usually differ; replaced_bytes is the count of old bytes actually overwritten (0 for the two insert verbs, which never overwrite), lines={start,end} is that same region as FILE:LINE, and trailing_newline_folded / separator_padded say what the seam rules did to the payload. region={start,end,context,text} is the post-edit region as it is ON DISK (the applied lines plus context=3 each side; over 2 KB it carries head, tail, elided_lines and capped=true) and blob_sha is the git blob id of the written bytes (== git hash-object FILE) — the Read an agent would make to see what landed is already in hand. The receipt also carries the POST-EDIT VERIFICATION the tool would otherwise tell you to run: edit_check={status,callers,incompatible,sites} — the same answer --edit-check=FILE:SYM gives, sites naming each broken caller's call LINES — and tests_to_run, the same rows --affected=FILE gives, run recipe included; and exactly ONE next= (a contract-change with broken callers: --uses=FILE:SYM; else the first run= recipe; else --test-gate=FILE; under --no-post-check: --edit-check=FILE:SYM). Edit, see what landed, verify and find the tests to run is ONE call.
**Try it**
_Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured)._
```
$ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h
{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":8589,"new_file_bytes":8677,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"sep … [line truncated: 742 more bytes on this line]
{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]},
{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]},
{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}],
"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true},"tests_to_run":[{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","run":"bash test/adaptivecutshapecheck.sh"},
{"p":"test/verify_csr.cpp","run":"bash test/a9disclosurecheck.sh"},
{"p":"test/verify_radix.cpp","run":"bash test/greptiercheck.sh"}],
"tests":3,"script_gates_unmodelled":581,"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"}
```
**Shaped by:** `--insert-after-symbol`
**Caveats (stated by the binary):**
- optional file-path substring disambiguating a same-named definition.
- replaced_bytes is the count of old bytes actually overwritten (0 for the two insert verbs, which never overwrite), lines={start,end} is that same region as FILE:LINE, and trailing_newline_folded / separator_padded say what the seam rules did to the payload.
- over 2 KB it carries head, tail, elided_lines and capped=true) and blob_sha is the git blob id of the written bytes (== git hash-object FILE) — the Read an agent would make to see what landed is already in hand.
### `--no-post-check`
**Answers:** skip that folded verification (the index refresh it needs is the one the next verb call would pay for anyway;
pass this when you are about to edit again immediately). The MCP spelling is post_check:false. Single-root only.
**Try it**
_The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr._
```
$ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check
{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":8501,"new_file_bytes":8589,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator_p … [line truncated: 570 more bytes on this line]
```
**Shaped by:** `--edit-target-file`
**Caveats (stated by the binary):**
- skip that folded verification (the index refresh it needs is the one the next verb call would pay for anyway;
### `--edit-plan=FILE`
**Answers:** versioned JSON multi-edit transaction: {version:1, edits:[{op,target,file?,payload}]};
op is one of replace_symbol_body, insert_before_symbol, insert_after_symbol each target takes the same forms as TARGET above (a name, an @FILE:LINE seed, a handle)
**Try it**
_Neither --dry-run nor --apply: the mode is explicit, so this refuses._
```
$ ./build/ripwire . --edit-plan=/aux/edit_plan.json
(empty)
```
### `--dry-run | --apply`
**Answers:** the plan's explicit mode: --dry-run preflights and prints the receipt without writing, --apply commits;
exactly one of the two is required. Payload paths are relative to the plan file and CONFINED to its directory: a path resolving outside it (an absolute path, a '..' escape, or a symlink pointing out) refuses, naming the path it resolved to, and the receipt's payload_path shows what each op will READ. Every target/payload/span is preflighted before any write; overlaps refuse. Apply holds sorted per-file locks and atomically renames each file, re-verifying EACH file's bytes immediately before ITS OWN write (recheck_before_each_write in the receipt) so a non-cooperating external writer is detected rather than clobbered. Prior files roll back on a later write failure or such a detection; the message says which happened and how many files it restored, and ends with the ONE call that shows the state — next: a git diff (exit-code mode) over the plan's files from ; exit 0 IS the claim, checked against git. A crash between file renames remains a disclosed limit.
**Try it**
_The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure._
```
$ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply
{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":fal … [line truncated: 503 more bytes on this line]
{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]},
{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]},
{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}],
"graph_ambiguous":6146,"graph_unresolved":3466,"counts_floor":true}}]}
```
**Shaped by:** `--edit-check`
**Caveats (stated by the binary):**
- Payload paths are relative to the plan file and CONFINED to its directory: a path resolving outside it (an absolute path, a '..' escape, or a symlink pointing out) refuses, naming the path it resolved to, and the receipt's payload_path shows what each op will READ.
- A crash between file renames remains a disclosed limit.
### `--safe-delete=SYM`
**Answers:** "can I delete this?" — ONE call composing signals the tool already computes for one already-resolved SYM: 1-hop callers=, the transitive --impact blast radius (impact_reaches=), every --uses read/write/import/call/extends site (uses=), how much of the blast radius the tested= lens covers (tested_self=/radius_tested=/radius_untested=), and --dead-code's own high-confidence shape at defs=1 (dead_code_candidate=).
ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges. FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict.
**Try it**
_Unknown-symbol refusal shape for --safe-delete._
```
$ ./build/ripwire . --safe-delete=DoesNotExist
(empty)
```
**Shaped by:** `--limit`
**Caveats (stated by the binary):**
- ambiguous_callers= names callers whose own calls include an ambiguously-resolved one (g.ambOut) — a caveat, not a count of proven-wrong edges.
- FACTS only: risk= names what was found — none-found (zero callers AND zero uses), untested-radius (a radius exists and none of it is test-covered), or uses-exist (a radius exists and some of it is tested) — never a go/no-go verdict.
### `--slice=SYM[:VAR]`
**Answers:** NAME-BASED intra-procedural def-use slice of variable VAR inside the ONE uniquely-resolved definition SYM (statement-level def-use edges as a queryable primitive — the ARISE result, arXiv:2605.03117).
One row per line touching VAR, source order: k=def|use|both| scope = a Python global/nonlocal statement, neither read nor write; t=param|decl| assign|call-arg|read|global|nonlocal = the strongest role on the line; CDATA = the trimmed source line; defs=/uses= count occurrences. JS/TS destructuring binders (`const {a, b} = o`, `[x] = arr`, destructured parameters) are locals whose def is the pattern line. A write hidden behind a call — receiver mutation, a by-reference/out-parameter, a function-like macro — is a use, never a def (stated in the legend). Bare --slice=SYM lists the sliceable locals ( rows) so a caller can pick VAR. LIMITS in the legend, not implied: no alias analysis. REACHING DEFINITIONS are FLOW-SENSITIVE inside the definition for C-family and Python (root reach="cfg": a def is killed by the next unconditional def on every path, defs join at if/elif/else, switch, loop back-edge, try/finally, for/while-else, match, #ifdef merges) and source-order for JS/TS/Go/Java/Rust (reach="linear", nothing joins); every use row carries rd= (the lines of the defs that reach it, "-" = none). The unit is the STATEMENT (uses read the entering state, defs apply after); a nested lambda/def body, ?:, short-circuit fold into their statement, goto is untracked, global/nonlocal is tracked like a local — each disclosed in the legend. Block scopes ARE separated: a name declared twice in the definition is two variables, each row of a shadowed name carries b= (the declaration line it binds to), the root bindings=, the inventory one per binding. SYM matching several definition sites REFUSES (exit 1) listing the file:name spellings that pick one, like --edit-check. Served: C/C++/ObjC (+CUDA/Metal), Python, JS/TS, Go, Java, Rust — other indexed languages refuse loudly (never an empty success). Single-root only. PREPROCESSOR (C-family): a `#if 0` body and the `#else` of `#if 1` are DEAD — dropped, the line count disclosed as preproc_rows=; every other conditional region (`#ifdef X`, `#ifndef X`, `#if defined(X)`, `#if EXPR`) is build-dependent and cannot be decided without the build's macro set, so its rows are KEPT and flagged pp="1", and a pp def never hides the unconditional def before it in a flow (both are reaching). LINE-SEEDED: --at=FILE:LINE beside --slice (or --slice=@FILE:LINE) is the ARISE (file, line[, variable]) seed — the definition sliced is the innermost one enclosing the line (a seed narrows an otherwise-ambiguous SYM; a seed enclosed by none of SYM's definitions refuses naming both). A seed line naming exactly ONE sliceable local pre-picks it (disclosed: seed= var_from="seed"); zero or several serve the inventory with seed_vars= and the candidate rows marked seed="1", never a guess. A plain identifier spec beside --at reads as the seed's VARIABLE (--slice=VAR --at=src/f.cpp:12). SINCE: --since=REV|DATE beside --slice=SYM:VAR adds a child carrying the DEPENDENCE diff of that variable against the committed tree at REV — one row per added or removed STATEMENT of the variable, one row per added or removed def-use edge. The unit is the STATEMENT and the key is the ROLE, never the line and never the text, so a re-wrap, a comment edit, an insertion above the definition, and a rename of an unrelated local all come back EMPTY. Empty means no def-use edge of that variable moved, never that the commit changed nothing — git diff answers the second question. status= names each way the symbol can be absent at REV, and comparable="0" says outright that no comparison was made and the emptiness is not evidence. Refused on the bare inventory: a dependence diff needs a seed variable.
**Try it**
_Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR._
```
$ ./build/ripwire . --slice=rankGraphTeleport
... [3 more line(s); run it to see the whole thing]
```
**Shaped by:** `--no-redact`, `--since`, `--slice-flow`, `--slice-depth`, `--at`
**Caveats (stated by the binary):**
- A write hidden behind a call — receiver mutation, a by-reference/out-parameter, a function-like macro — is a use, never a def (stated in the legend).
- LIMITS in the legend, not implied: no alias analysis.
- SYM matching several definition sites REFUSES (exit 1) listing the file:name spellings that pick one, like --edit-check.
### `--slice-flow=back|fwd|both`
**Answers:** TRANSITIVE cross-statement data-flow slice (modifies --slice=SYM:VAR;
refused alone or on the bare inventory — a flow needs a seed variable). Follows VALUE FLOW over reaching-definition def-use edges — a use of v reaches the last def of v in source order before it — by bounded BFS from the seed variable, the ARISE paper's own slicer semantics (arXiv:2605.03117: seed + direction, bounded BFS, stops at the function boundary; the inter-procedural half stays with --callers/--impact by the paper's own design). back = statements whose values feed the seed; fwd = statements the seed's value reaches; both = the union. Flow rows are : v= the variable at that step, d= BFS depth (seed rows are depth 0), f= the line the step was reached from. steps= counts flow rows; depth= states the bound in force. LIMITS (in the legend too): name-based, no alias analysis, line-granular ROWS (a multi-statement line merges) over statement-anchored CHAINING (a multi-LINE statement chains as ONE unit), a shadowed name's bindings walk separately (never into each other's block), data dependence only — no control dependence (the guard deciding whether a def executes is never a row).
**Try it**
_Forward flow: which statements the seed's value reaches, at the default depth bound._
```
$ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd
teleport( pw.begin(), pw.end() );]]>
... [23 more line(s); run it to see the whole thing]
```
**Shaped by:** `--slice-depth`
**Caveats (stated by the binary):**
- refused alone or on the bare inventory — a flow needs a seed variable).
### `--slice-depth=N`
**Answers:** the --slice-flow BFS depth bound, 1..32 (default 8, always disclosed as depth= on the root).
A bound that cuts a live frontier is disclosed as flow_truncated="1" — a short slice means "bounded here", never "nothing further exists". Refused without --slice-flow.
**Try it**
_--slice-depth without --slice-flow is refused loudly rather than silently ignored._
```
$ ./build/ripwire . --slice-depth=3
(empty)
```
**Caveats (stated by the binary):**
- A bound that cuts a live frontier is disclosed as flow_truncated="1" — a short slice means "bounded here", never "nothing further exists".
- Refused without --slice-flow.
### `--at=FILE:LINE`
**Answers:** the ENCLOSING-DEFINITION CHAIN at one location (1-based line), outermost->innermost — for when you hold a compiler error / diff hunk / stack frame, not a name.
rows, indexed definitions only; sym= names the innermost. The SAME seed composes into any SYM selector as @FILE:LINE (--callers=@src/f.cpp:120, --expand=@..., --edit-check=@..., --slice=@FILE:LINE:VAR, --replace-symbol-body=@... and the other edit TARGETs, ...) and resolves to that innermost definition — the no-name half of the file:line:name grammar. The NAME-scan verbs --mentions/--owners rebind a seed to that definition's name and answer, disclosing sym=. Beside --slice, the at flag is that verb's LINE SEED instead of a competing verb (see --slice). A malformed seed, an ambiguous or unmatched path, a line past EOF, a line inside no indexed definition, or two disjoint definitions sharing the line each REFUSE with a specific diagnosis (exit 1) — never a guess, never an empty chain.
**Try it**
_Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._
```
$ ./build/ripwire . --at=src/graph.h:1148
```
**Shaped by:** `--slice`
**Caveats (stated by the binary):**
- the ENCLOSING-DEFINITION CHAIN at one location (1-based line), outermost->innermost — for when you hold a compiler error / diff hunk / stack frame, not a name.
- A malformed seed, an ambiguous or unmatched path, a line past EOF, a line inside no indexed definition, or two disjoint definitions sharing the line each REFUSE with a specific diagnosis (exit 1) — never a guess, never an empty chain.
### `--pr-context[=BASEREF]`
**Answers:** no-LLM review-evidence bundle for the diff (working-tree, or vs BASEREF): per changed file, its symbols + callers + blast radius + affected tests + co-change partners + owners.
The bundle is BUDGETED by default (8000 tokens, budget_default="1" on the root; --token-budget=N or --max-tokens=N set it explicitly): per-file structural counts survive first, the deep detail (caller/co-change lists, per-symbol rows) trims deepest-first, truncated= names what was dropped and est_tokens= reports the fit. When even the structural floor of every changed file exceeds the budget, the FILES (blast-radius order) are windowed: shown=/capped=/total=/ next_offset= disclose the cut and next= pastes the next page (--offset=N / --limit=N window them explicitly). ANCHORING: the BASEREF form diffs against merge-base(BASEREF,HEAD), never BASEREF's tip — "what did THIS work change since it forked", not "how do the two trees differ today". base_moved= counts the paths BASEREF moved since the fork that this work never touched (excluded, not silently); anchor="ref-tip-two-dot" = no merge-base (unrelated history). direction= always names the SIDE you are reading, and a no-ref-work row fires when BASEREF's tip IS the merge base -- it carries no divergent work, so every row is HEAD's.
**Try it**
_No-LLM review-evidence bundle for the working-tree diff — recorded against a DIRTY tree, so it is populated rather than empty._
```
$ ./build/ripwire . --pr-context
```
**Shaped by:** `--test-gate`, `--from-trace`, `--limit`, `--map-diff`, `--index-out`
**Caveats (stated by the binary):**
- --token-budget=N or --max-tokens=N set it explicitly): per-file structural counts survive first, the deep detail (caller/co-change lists, per-symbol rows) trims deepest-first, truncated= names what was dropped and est_tokens= reports the fit.
- When even the structural floor of every changed file exceeds the budget, the FILES (blast-radius order) are windowed: shown=/capped=/total=/ next_offset= disclose the cut and next= pastes the next page (--offset=N / --limit=N window them explicitly).
- ANCHORING: the BASEREF form diffs against merge-base(BASEREF,HEAD), never BASEREF's tip — "what did THIS work change since it forked", not "how do the two trees differ today".
### `--merge-scout=REF[,REF...]`
**Answers:** read-only cross-branch overlap: for each REF, the symbols it changed vs its merge-base with HEAD (git-archive TEMP copies — never checked out, never mutates a ref);
the dirty working tree joins as an implicit extra arm. Pairwise: a changed symbol on TWO arms is a same-symbol conflict, two arms touching different symbols in the same file is a textual risk; is the fewest-conflicts-first greedy land order (ties: ref name asc). An unresolvable REF refuses loudly (exit 1, names the ref) before any archive work. ANCHORING: every arm is diffed against its OWN merge-base with HEAD, never against live HEAD — a file an arm never opened can never show up because the live line moved. head_conflicts= is what that anchor hides, kept as its own row class: symbols this arm changed that the LIVE LINE also changed since the arm forked (HEAD is not an arm, so no pairwise comparison can see it). Single-root only.
**Try it**
_Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared)._
```
$ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1
... [2 more line(s); run it to see the whole thing]
```
**Shaped by:** `--plan-lanes`, `--plan`
**Caveats (stated by the binary):**
- read-only cross-branch overlap: for each REF, the symbols it changed vs its merge-base with HEAD (git-archive TEMP copies — never checked out, never mutates a ref);
- An unresolvable REF refuses loudly (exit 1, names the ref) before any archive work.
- ANCHORING: every arm is diffed against its OWN merge-base with HEAD, never against live HEAD — a file an arm never opened can never show up because the live line moved.
### `--plan-lanes=N --task=GOAL`
**Answers:** PRE-HOC lane plan: BEFORE a line is written, if this task is split across N isolated worktrees (N=2..16), which lanes would COLLIDE and in what order should they land.
Where --merge-scout says "these branches already conflict", this says "these lanes WOULD conflict if assigned this way" — no ref to resolve, no archive, no re-ingest. JSON on stdout, always (redirect it: > .ripwire_lanes.json); ripwire writes no file. Exit 0 whenever a plan was produced, INCLUDING when conflicts are predicted (conflicts are data, and the landing order exists to handle them); exit 1 only for refusals. A claim keys on path+scope+name, never on id= (id degrades to a bare NAME when no scope was captured, so free functions in different files would collide); id= is carried per row for addressability, null when it would be bare, with id_addressable saying so. Three separate pair classes: conflicts[] (same claim key on both lanes — git will fight), same_file_risk[] (different keys, same file, aggregated per file), contract_touch[] (one lane's claim sits in another's blast radius — an adaptation, NOT a merge conflict). The conflict test runs on CLAIMS, never on blast radii. warnings[] carries every honest limit in band with a stable code. Each lane also carries an advisory execution object: the current Codex model + reasoning effort, selecting rule, exact structural signals and caveats under policy=codex-lane/v1. basis=structural-only: it does NOT understand task semantics, runtime behavior or security sensitivity; the orchestrator must override those cases. Single-root only. AUTO-CARVE SPLITS THE RANKED SURFACE, NOT YOUR SENTENCE: if your task has enumerable parts, use --brief and write one line per part.
**Try it**
_Out-of-range refusal shape for the lane count._
```
$ ./build/ripwire . --plan-lanes=99 --task=x
(empty)
```
**Shaped by:** `--legend`, `--json`
**Caveats (stated by the binary):**
- Exit 0 whenever a plan was produced, INCLUDING when conflicts are predicted (conflicts are data, and the landing order exists to handle them);
- A claim keys on path+scope+name, never on id= (id degrades to a bare NAME when no scope was captured, so free functions in different files would collide);
- Three separate pair classes: conflicts[] (same claim key on both lanes — git will fight), same_file_risk[] (different keys, same file, aggregated per file), contract_touch[] (one lane's claim sits in another's blast radius — an adaptation, NOT a merge conflict).
### `--plan-lanes --brief=FILE`
**Answers:** the explicit form of the above: one non-blank line per lane, N = the line count.
Each line is ranked on its own — no community carve, no bin packing — so the lane boundaries are the ones you wrote. This is the mode whose precision is defensible; prefer it when you can. Lane isolation is a QUALITY argument, not a speed one (CAID, arXiv 2603.21489: 63.3% vs 55.5% shared, largest gains on weaker lane models — and wall clock got WORSE).
**Try it**
_NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode)._
```
$ ./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt
add a --since filter to the doc-drift verb
add the CLI parse arm and help text for the new filter
write regression tests for the new filter
```
**Shaped by:** `--legend`, `--json`
**Caveats (stated by the binary):**
- Lane isolation is a QUALITY argument, not a speed one (CAID, arXiv 2603.21489: 63.3% vs 55.5% shared, largest gains on weaker lane models — and wall clock got WORSE).
### `--stray-content[=SUBSTR]`
**Answers:** "where does this content live?" across ALL branches — the question `git cherry` cannot answer.
Per local ref (SUBSTR filters ref names): the lines its own divergent work AUTHORED vs its merge-base with HEAD that the live line does NOT have, and a verdict. v="unmerged" = genuinely absent; v="superseded" = the live line removed the SAME base code this ref removed, i.e. it re-implemented the work (git cherry still calls that commit unmerged, forever); v="merged" refs are omitted. Every row shows its raw del=/redone=/sim= evidence, so a verdict is auditable, not a black box. v="unknown" (ok="0") = the ref has NO merge-base with HEAD, so it could not be analysed at all — a shallow clone (the actions/checkout DEFAULT) puts every ref here. It is NOT a claim the work is merged: it is the absence of an answer, counted in its own unknown= bucket so unmerged+superseded+merged+unknown always reconciles with refs=, and surfaced by --plan as an row rather than silently dropped. LIMITS: line-granular, not semantic — a rewrite that shares no deleted base line reads as unmerged; binary/oversized blobs are reported diffable="0" with no counts. Read-only (cat-file/diff/ls-tree); single-root only.
**Try it**
_Which lane-* refs still hold divergent authored work vs HEAD, with verdicts._
```
$ ./build/ripwire . --stray-content=lane
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--plan`, `--abi`, `--whereis`, `--limit`, `--eval-stray`
**Caveats (stated by the binary):**
- "where does this content live?" across ALL branches — the question `git cherry` cannot answer.
- Every row shows its raw del=/redone=/sim= evidence, so a verdict is auditable, not a black box.
- It is NOT a claim the work is merged: it is the absence of an answer, counted in its own unknown= bucket so unmerged+superseded+merged+unknown always reconciles with refs=, and surfaced by --plan as an row rather than silently dropped.
### `--plan`
**Answers:** (with --stray-content) "of all my branches, which still hold REAL work, and in what order should I land them?" Selects the refs --stray-content calls v="unmerged", DROPS the v="superseded" ones (landing them would re-do work the live line already did — the exact waste --stray-content exists to catch), and feeds the survivors to --merge-scout's existing pairwise-conflict + fewest-conflicts-first landing-order machinery — composition only, neither verb's logic is reimplemented.
is unmerged work NOT fed to merge-scout THIS run (a cost bound, not a verdict); names the superseded drops and why. COST: --stray-content is a cheap per- blob sweep, but --merge-scout is per-ARM (git-archive + full ingest of each ref's tree) — measured 27s for 9 unmerged refs on a 35-branch real C++ repo (~3s/ref). kMaxPlanScout (12) bounds it to the top-N unmerged refs BY STRAY SIZE; --detail lifts the bound to scout everything. This is an EXPLICIT opt-in "before you land" call, not a per- question one — the default map's ~0.10s path is untouched. Read-only; single-root only.
**Try it**
_Select the genuinely-unmerged refs of one family and feed them to merge-scout for a landing order (a merged family yields an empty landing set — still a measurement, disclosed on the root)._
```
$ ./build/ripwire . --stray-content=lane/ --plan
... [21 more line(s); run it to see the whole thing]
```
**Shaped by:** `--stray-content`, `--legend`, `--json`
**Caveats (stated by the binary):**
- is unmerged work NOT fed to merge-scout THIS run (a cost bound, not a verdict);
- This is an EXPLICIT opt-in "before you land" call, not a per- question one — the default map's ~0.10s path is untouched.
### `--abi`
**Answers:** (with --stray-content) the CROSS-BRANCH ABI-BREAK gate `--layout` and `--stray-content` each miss alone: a branch that adds one field to a dual-compile uniform struct merges textually clean and reads as a harmless "+1 field" to a line-granular diff.
SCOPE is what each ref AUTHORED — the paths `git diff base..tip` reports against its own MERGE BASE, never `diff HEAD..tip`. A file the branch never opened cannot be a break the branch introduced, and on a long-lived shared tree that one distinction is nearly all the noise: measured on a 35-branch C++ repo, 487 drift rows fell to 4 (the rest were the live line's own evolution reflected back at the reader). For each authored path this runs --layout's OWN field-offset arithmetic lexically on that ref's git blob (never indexed) and compares it against HEAD's computed fields. LISTED by default: kind="drift" (the byte contract differs — the only kind that exits 2), kind="unknown" (a ref-side copy this module could not model; its caveats ride along in ref_caveat and it is NEVER reported as unchanged), kind="absent" (the ref does not define the struct at that path at all). EXCLUDED by default, each on its own header counter — add --detail=N to print them: kind="rename" (identical slots and field TYPES under different field NAMES: every byte stayed where it was, so it is a source change, not a byte-contract one — note a same-type field REORDER is lexically indistinguishable from a rename and lands here too), kind="spelling"/"stub" (--layout's own harmless cases), and kind="head-moved" (the ref's copy equals its own merge-base copy, so the LIVE LINE changed, not the branch). head_only= counts candidate sites on paths only the live line touched; unmodelable= counts sites skipped because HEAD's own copy carries no baseline; rows=/ shown=/dropped=/excluded= reconcile the body against the sweep (capped="0|1" is the tool-wide truncation BIT; dropped= is the count). Nothing is dropped without a number. Structs that match are omitted (report only differences); a ref with no rows at all counts into quiet=, a ref whose every row is an excluded kind counts into excluded_refs= (and prints under --detail=N), and broken_refs= counts REFS (not rows). Rows are ranked by SIZE DELTA so the biggest contract break leads, capped at 12 per ref with an explicit ; --detail=N lifts the cap. LIMITS: HEAD's own side is the WORKING TREE's --layout answer, not a re-fetched git blob at HEAD's commit (the same scope --layout itself claims); a nested field's OWN type resolves through HEAD's copy even when the ref also changed it; the ref-side locator is index-free and file-scope (one namespace deep) only, so a struct nested in a class or an extern "C" block reads absent rather than compared; a HEAD-side struct --layout itself cannot model at all (pragma pack, bitfields, ...) has no baseline and is counted in unmodelable=, not compared; the authorship anchor is per PATH, so a branch changing struct S in one file while the live line changes S's mirror in another is a merge hazard only `--layout=S` on the merged result can see. Read-only; single-root only.
**Try it**
_Cross-branch ABI-break gate: struct byte-contract drift on each ref's AUTHORED paths._
```
$ ./build/ripwire . --stray-content=lane --abi
```
**Caveats (stated by the binary):**
- SCOPE is what each ref AUTHORED — the paths `git diff base..tip` reports against its own MERGE BASE, never `diff HEAD..tip`.
- A file the branch never opened cannot be a break the branch introduced, and on a long-lived shared tree that one distinction is nearly all the noise: measured on a 35-branch C++ repo, 487 drift rows fell to 4 (the rest were the live line's own evolution reflected back at the reader).
- For each authored path this runs --layout's OWN field-offset arithmetic lexically on that ref's git blob (never indexed) and compares it against HEAD's computed fields.
### `--whereis=SYM`
**Answers:** which REF's tree defines or mentions SYM — HEAD first, then every local branch, with on-head="0" naming the case the verb exists for: content that lives only on a branch.
Each distinct blob is read ONCE (content-addressed), so N branches cost ~one tree. kind="def" on a HEAD row is the PARSED index's answer (head_labels="index"); on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition. head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD). refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count. Read-only; single-root only. LIMITS: a TREE scan finds only what some ref STILL carries, so hits="0" alone cannot tell a name this repo never had from one it deleted, and content dropped by every tree is invisible. Add --with-history: a row then says v="never" or v="removed" with the commit, date and file that removed it. Remote-tracking refs are excluded (they mirror local ones); refs are capped, narrow with --stray-content=SUBSTR.
**Try it**
_Which ref's tree defines or mentions SYM — HEAD first, then every local branch._
```
$ ./build/ripwire . --whereis=rankGraphTeleport
... [23 more line(s); run it to see the whole thing]
```
**Shaped by:** `--with-history`, `--limit`
**Caveats (stated by the binary):**
- on a REF row it is a LEXICAL heuristic — ref blobs are raw text, never ingested, so a doc quoting a signature still reads as a definition.
- head_labels="lexical" ⇒ HEAD fell back to that heuristic too (no indexed def of the name, or a working tree that drifted from HEAD).
- refs_scanned= is the SCAN denominator (refs read besides HEAD), not a matched count.
### `--flags[=SUBSTR]`
**Answers:** the dark-content dashboard: what is BUILT but OFF in this repo.
Harvests all three gate patterns — #ifndef/#define header gates, CMake option(), and getenv() reads — and reports gate, kind (compile/cmake/env), default, the size of the code it guards (#if regions and their LOC), and its read sites. When a name is BOTH a header gate and a CMake option the CMake default WINS (that is what the build actually passes) and both sites are listed. A gate whose default IS another gate's name (#define F_WALLS F_ALL) is resolved: it inherits the master's default and rolls its guarded size up, so a master switch shows rather than a misleading loc="0". LIMITS: lexical, not preprocessed — a gate computed at configure time or set only in a CI script shows its in-repo default, never the value your build used. A gate needs a VALUE (#ifndef F / #define F 0) to be a gate: valueless pairs are include guards and are excluded, and a gate read as a VALUE (constexpr bool k = F != 0, then if constexpr) reports regions="0" honestly — its code is a C++ branch, not an #if region. Pair it with --flip=NAME below to size ONE gate instead of listing them all.
**Try it**
_The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates._
```
$ ./build/ripwire . --flags
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--flip`
**Caveats (stated by the binary):**
- LIMITS: lexical, not preprocessed — a gate computed at configure time or set only in a CI script shows its in-repo default, never the value your build used.
- A gate needs a VALUE (#ifndef F / #define F 0) to be a gate: valueless pairs are include guards and are excluded, and a gate read as a VALUE (constexpr bool k = F != 0, then if constexpr) reports regions="0" honestly — its code is a C++ branch, not an #if region.
### `--flip=NAME`
**Answers:** (with --flags) the BLAST RADIUS of turning ONE gate ON: which code becomes live, how much, which SYMBOLS hold it, what those transitively reach, and which TESTS cover it — the actionable sequel to --flags' list.
Reports #if regions AND the C++ branch sites a value-style gate governs (constexpr bool k = F != 0, then if constexpr( k )): the binding is followed, so the family --flags honestly sizes at regions="0" gets a real radius here. Alias chains run BOTH ways — flipping a MASTER rolls up every child that #defines to it ( rows), flipping a CHILD lights only that child and names the plus the siblings its flip would add. kind=cmake means the switch becomes a -DNAME=1 compile definition, so the C++ radius is identical, but it ALSO steers the build graph (an if(NAME) target_sources can add whole files) — those CMake sites are listed as rows and deliberately NOT followed. kind=env is RUNTIME (runtime="1"): there is no delimited region, so the hosts are the symbols that consult the variable and every row is conditional at its read. --detail lifts the per-list row caps. LIMITS: lexical and single-line, never preprocessed. A binding split across two lines is missed, and block comments are only skipped line-by-line. The value lane reads C-family source only and treats a file that declares its OWN constant of the same name as shadowing the gate's (C++ scoping) — but a third header's same-named constant, included rather than redeclared, would still be counted. A lit site inside no indexed def (a guarded member field, a file-scope constexpr, a test-macro body) counts into filescope= instead of a host. Single-root only (the harvest reads on-disk paths, which a merged workspace relabels) — run it per root. Exit 0 always otherwise: a report, not a gate; an unknown gate name refuses (exit 1) and names the near-misses.
**Try it**
_Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN)._
```
$ ./build/ripwire . --flags --flip=RIPWIRE_ASA
(empty)
```
**Shaped by:** `--flags`
**Caveats (stated by the binary):**
- kind=env is RUNTIME (runtime="1"): there is no delimited region, so the hosts are the symbols that consult the variable and every row is conditional at its read.
- LIMITS: lexical and single-line, never preprocessed.
- A binding split across two lines is missed, and block comments are only skipped line-by-line.
### `--layout=STRUCT`
**Answers:** the CPU/GPU contract view for ONE struct/class: its fields in declaration order with COMPUTED offsets/sizes/padding, every static_assert in the index that mentions it, and EVERY same-name definition compared field-by-field (the mirror/stub drift check that a dual-compile uniform block needs on every edit).
file:name disambiguates a same-named struct (like --around/--lego). Exit 2 when the contract is BROKEN: mirror="mismatch" (two definitions of the name disagree) or agree="0" (a sizeof tripwire contradicts the computed size). Multi-root aware: the mirror check spans every merged root. LIMITS, read them: the offsets are a MODEL, not the ABI — a lexical walk under standard- layout assumptions on a 64-bit Apple/LP64 target (natural alignment, interior padding, trailing pad to the aggregate's own alignment). It is NOT a compiler: #pragma pack, bitfields, virtuals, base classes, nested/anonymous aggregates, #if-conditional members, templates, pointer-to-member fields and any field type it cannot size all set modeled="0" with a named caveat instead of printing a number, and one unsized field un-places every field after it. alignas(N) and attribute packed ARE modelled. Array extents and macro type names resolve against the DEFINING FILE's own #define/constexpr constants only, and a macro with two definitions is accepted only when both agree on the size (the dual-compile half/__fp16 case). Definitions and asserts come from the INDEXED C-FAMILY files only (a TypeScript/Swift class has no byte layout) — .metal IS one of them (indexed under the C++ grammar, see kLangTable), so a Metal struct's layout is modelled like any other C-family aggregate.
**Try it**
_The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated._
```
$ ./build/ripwire . --layout=Lang
(empty)
```
**Shaped by:** `--abi`, `--field-affinity`
**Caveats (stated by the binary):**
- file:name disambiguates a same-named struct (like --around/--lego).
- LIMITS, read them: the offsets are a MODEL, not the ABI — a lexical walk under standard- layout assumptions on a 64-bit Apple/LP64 target (natural alignment, interior padding, trailing pad to the aggregate's own alignment).
- It is NOT a compiler: #pragma pack, bitfields, virtuals, base classes, nested/anonymous aggregates, #if-conditional members, templates, pointer-to-member fields and any field type it cannot size all set modeled="0" with a named caveat instead of printing a number, and one unsized field un-places every field after it.
### `--field-affinity[=STRUCT]`
**Answers:** the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART.
Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the report to one. Pairs carry Chilimbi's separation weight wt = (64 - dist)/64 (Cache-Conscious Structure Definition, PLDI 1999) — CITED, not invented here, along with the affinity graph and the points-to-free access enumeration; the advice-not-transform posture is Hundt et al., CGO 2006. Exactly TWO findings fire, both with a direction you can defend in one sentence: split-line (two fields co-accessed by 2+ functions at wt 0.00, so NO field order puts them on one line) and straddle (one co-accessed field crossing a line boundary). ADVICE ONLY: it never proposes a reordering and it has no rewrite mode, because pack-tighter/sort-by-size advice is NON-MONOTONIC (tight packing can induce false sharing — the reason the Go team keeps its own fieldalignment analyzer out of vet and gopls). LIMITS, both in the header: static access counts are NOT dynamic frequency, so fns= is a FLOOR of distinct indexed functions and w= is a call-graph reachability PROXY (1 + fan-in), never a measured count; only dot/arrow member syntax is counted (a bare field name inside its own method is indistinguishable from a local); a field name declared by TWO aggregates is REFUSED and tallied in amb_skipped= rather than guessed; and all geometry is the LP64 MODEL, so a definition --layout marks modeled="0" contributes its affinity graph and NO geometry finding. validate= names the instrumented PROFILE_SCOPE whose hardware counters would confirm the hypothesis (see docs/FIELDAFFINITY.md for the worked example). C/C++/ObjC only. Exit 0 always: a report, not a gate.
**Try it**
_The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite._
```
$ ./build/ripwire . --field-affinity
... [17 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- ADVICE ONLY: it never proposes a reordering and it has no rewrite mode, because pack-tighter/sort-by-size advice is NON-MONOTONIC (tight packing can induce false sharing — the reason the Go team keeps its own fieldalignment analyzer out of vet and gopls).
- LIMITS, both in the header: static access counts are NOT dynamic frequency, so fns= is a FLOOR of distinct indexed functions and w= is a call-graph reachability PROXY (1 + fan-in), never a measured count;
- a field name declared by TWO aggregates is REFUSED and tallied in amb_skipped= rather than guessed;
### `--doc-drift[=SUBSTR]`
**Answers:** which of this repo's DOC claims are now false.
Verifies the CHECKABLE anchors in every markdown file (SUBSTR filters doc paths) against the live index and prints ONLY the ones that no longer hold, four kinds: file:line refs (why="missing-file" the path is gone, "past-eof" the file is shorter than that, "line-moved" the line is no longer inside the symbol the doc names beside it — got= names the squatter); backticked symbol mentions ("undefined"); `= N` constants ("const-value"); and `[N]` array extents ("array-extent"). LIMITS, stated because a doc-drift verb that cries wolf is worse than none — every lane deliberately UNDER-reports. A backticked name is called stale only when it occurs nowhere in any non-markdown file as an identifier token, so every library name is silent, and so is any repo constant the grammar does not tag as a definition (namespace-scope constexpr in C++, for one) — those are counted as unchecked r="not-a-definition", never as drift. A number is compared only against a DECLARATION-shaped integer literal (a decl keyword on the line, or the name opening it) that the corpus binds UNIQUELY; two values in the tree means unchecked, not drift. A `NAME = N` whose NAME appears nowhere in the code is prose, counted in prose= and never claimed as an anchor. Symbol mentions inside ``` fences are skipped (illustrative code, not claims). checked + unchecked = anchors, always: whatever was not proved says so in an row. Read why="undefined" precisely — it says the name is defined NOWHERE in this repo, which is not the same as DELETED: in a plan or design doc naming work not yet built, that is expected rather than rot. The file:line, const and array lanes are the high-precision ones; the mention lane is the weakest — --with-history is the fix, splitting it into why="deleted" (history removed the name; got= names the commit and date, at= the file) versus unchecked r="never-in-history" (this repo never had it, so it is not rot at all). DATED RECORDS vs ROT. An audit's finding row and a live map gone stale look identical — both are "the code moved and the doc did not" — so a failed anchor the AUTHOR DATED is split out as kind="dated-record" and counted in dated=, leaving drift= for the LIVE rot. drift + dated is every anchor that failed: a record still prints, it is never dropped. rec= names the evidence, most specific first: "line" (the line itself hedges — an at-the-time / as-of-DATE note, or a row opening with an ISO date), "block" (the nearest heading carries an ISO date), "title" (the filename or H1 does), "stamp" (a LABELLED front-matter self-date: 'Date: …', 'Written …', 'Generated: …'). WHAT THIS LANE CANNOT DO, because both were measured and rejected: it cannot use git history — 90 of this repo's 98 stale file:line anchors were CORRECT at their own doc's last commit, audit findings and live design docs alike, because "was it true when written" is the definition of BOTH a record and rot; and it will not read a bare date in the opening prose, which on this repo alone dated three LIVE documents on a day they merely mentioned. It reads dating MARKS, so a doc that is obviously an artifact-of-a-date to a human but never writes that date machine-readably reports LIVE (this repo has two). The bias is one-directional on purpose: a wrong "record" hides real rot, a wrong "live" only over-reports. An inception or freshness date ('opened …', 'Last updated …') is a claim the doc is CURRENT and never marks a record. NOT CHECKED AT ALL: prose, Status lines, dates, 'N of M done' tallies, and whether a code block's body is still correct. Always exits 0 — a report, not a gate. Root element carries at="[+dirty][+shallow]" (omitted on a non-git root) — the commit these counts were computed against, so a number quoted from this report stays comparable across a HEAD that moves mid-session.
**Try it**
_Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp)._
```
$ ./build/ripwire . --doc-drift
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit`
**Caveats (stated by the binary):**
- LIMITS, stated because a doc-drift verb that cries wolf is worse than none — every lane deliberately UNDER-reports.
- A `NAME = N` whose NAME appears nowhere in the code is prose, counted in prose= and never claimed as an anchor.
- Symbol mentions inside ``` fences are skipped (illustrative code, not claims).
### `--doc-drift --gateability`
**Answers:** turn "CI stays non-gating" into a finishable to-do list: for every doc that STILL has a LIVE (undated) failing anchor, prints its path and live=N (how many of its rows a date would fix), plus projected_drift= — repo-wide drift= if EVERY listed doc got the fix.
The fix is always the same one this lane already reads for rec="title"/"stamp": an ISO date in the doc's H1/filename, or a front-matter self-date line (Date:/Written:/ Generated:/Recorded:/Reviewed:/Audited:/Authored:). projected_drift= is an UPPER BOUND, not a mandate — dating a doc that is genuinely a live/current reference (not a snapshot-in-time record) would hide real rot rather than honestly classify it. Requires --doc-drift (refused loudly alone).
**Try it**
_Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp)._
```
$ ./build/ripwire . --doc-drift
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--recall`, `--comment-coherence`, `--with-history`, `--plan-lint`, `--limit`
**Caveats (stated by the binary):**
- projected_drift= is an UPPER BOUND, not a mandate — dating a doc that is genuinely a live/current reference (not a snapshot-in-time record) would hide real rot rather than honestly classify it.
- Requires --doc-drift (refused loudly alone).
### `--with-history`
**Answers:** OPT-IN: let --doc-drift and --whereis ask git HISTORY whether a name was ever in this repo, and which commit removed it.
ONE `git log -p` walk over everything reachable from HEAD, tokenizing removed lines — the pickaxe's semantics without the pickaxe's cost (`git log -S` per name is ~126 s at 247 names on a 2900-file repo; this is ~3 s, and ~0.8 s on ripwire itself). Off by default because those default paths run in 0.64 s and 0.15 s. Memoized per (repo, HEAD sha) — a commit is immutable, so the cache cannot go stale — and the blob covers the WHOLE repo, so a second question on the same commit costs a cache load, and --whereis reuses whatever --doc-drift already built. LIMITS: it walks HEAD's own history, so a name that only ever lived on an unmerged branch reads as never here (use --whereis's tree scan for that); a deletion performed ONLY as a merge resolution is not seen (merge diffs are not walked); and evidence is a removed LINE carrying the name, so a name whose last removal was from a doc rather than code is reported with that doc as its site. A repo deeper than the walk bound reports truncated="1" and answers unknown — never "never" — for anything it did not reach.
**Try it**
_Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed._
```
$ ./build/ripwire . --doc-drift --with-history
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--whereis`, `--doc-drift`
**Caveats (stated by the binary):**
- Memoized per (repo, HEAD sha) — a commit is immutable, so the cache cannot go stale — and the blob covers the WHOLE repo, so a second question on the same commit costs a cache load, and --whereis reuses whatever --doc-drift already built.
- LIMITS: it walks HEAD's own history, so a name that only ever lived on an unmerged branch reads as never here (use --whereis's tree scan for that);
- A repo deeper than the walk bound reports truncated="1" and answers unknown — never "never" — for anything it did not reach.
### `--plan-lint=FILE`
**Answers:** the house PLAN/DESIGN format's STRUCTURE check — never semantics, that stays --doc-drift's job.
FILE is read directly (like --from-trace's FILE, not through the crawled index), so it need not live inside any indexed root. GRAMMAR, narrow and opt-in on purpose (real house plans do not converge on one dialect): a card is exactly an H3 heading opening with a task id ("T" + 1-4 digits + up to 3 letters, e.g. T5 / T10 / T7b); a status ledger is exactly one heading (any level) whose text, stripped of a leading section mark, reads "Status" case-insensitively. A card's status is satisfied EITHER by a glyph on the LAST non-blank line of its own body OR by a ledger line naming its id (folded by digits — a bare card "T7" is also answered by a lettered ledger mention "T7a"/"T7b") that itself carries a glyph; the card's own body wins when it has one. A file showing NEITHER an H3 card NOR a ledger heading is reported dialect="0" with nothing further checked — not a failing lint, since most real plans are exactly that file. Once dialect="1": a card whose status did not resolve is status="missing" with why="unlaunched" (a ledger exists and never names this id or a lettered sub-task of it — the mid-wave "this task was never launched" catch), why="unresolved" (the ledger names it with no glyph nearby), or why="no-glyph" (no ledger exists in this document at all); an hourglass line whose git-blamed commit sits more than stale_commits= commits behind HEAD is stale="1" (never claimed outside a git repo — see git=; blames whichever line the status resolved to, named by src="ledger" when that is the ledger); a task id named in the ledger's own body with no matching card (same digit fold) is a ledger-orphan; a literal owed/OWED mention with no check-mark or cross anywhere LATER in the SAME document is undischarged (no cross-document tracking — a successor plan's discharge is invisible here, a stated limit, and this is substring matching with no semantic disambiguation: a doc that merely QUOTES the words reads the same as a real marker). Every gating row carries gating="1"; NOT CHECKED AT ALL: whether a card's claims are true, any heading level other than three for a card, a ledger heading spelled any other way, and a document that uses card headings as plain labels with NO status mechanism anywhere (no ledger, no glyph) — every card there reads "missing" too, a known, disclosed gap. Exit 2 when dialect="1" and gating is non-zero (unlike --doc-drift's always-0 report — nothing here has a legitimate "dated on purpose" reading); exit 0 clean or dialect="0"; exit 1 only when FILE could not be read.
**Try it**
_The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates._
```
$ ./build/ripwire . --plan-lint=test/planlintfix/wave.md
```
**Caveats (stated by the binary):**
- the house PLAN/DESIGN format's STRUCTURE check — never semantics, that stays --doc-drift's job.
- A file showing NEITHER an H3 card NOR a ledger heading is reported dialect="0" with nothing further checked — not a failing lint, since most real plans are exactly that file.
- an hourglass line whose git-blamed commit sits more than stale_commits= commits behind HEAD is stale="1" (never claimed outside a git repo — see git=;
### `--from-trace=FILE`
**Answers:** map a stack trace / sanitizer report / compiler-error text ('-'=stdin) onto the indexed symbols: table-driven frame extraction (python / asan / node / compiler / generic), ranked INNERMOST-first over in-corpus frames only (out-of-corpus frames are listed and counted, never ranked).
Each frame binds by its own NAME first (resolved_by="name") and falls back to the def enclosing its line (resolved_by="line") only when the name is absent/unknown/ambiguous — a trace older than the checkout therefore lands on the symbol it names, and a name-vs-line disagreement is disclosed as line_encloses=, never silently rebound. The counters close: in_corpus = suspects + merged + unresolved, with one row per file-matched frame no resolver could place. p= on a frame is the TRACE's own path:line; definition sites are the l= values. Emits the same bundle shape as --for — top suspects' signatures + the innermost in-corpus symbol's FULL body; composes with --token-budget, and HONORS --max-tokens=N (it bounds the bodies) — one of the six shapes that do, alongside the default map, --recall, --connect, --pr-context and --for --detail=N. --top-k is NOT read here (the frame order is the trace's, not a rank). TEST-TO-SOURCE HOP: when the innermost in-corpus frame is a TEST symbol — a failing-test trace names the assertion, not the subject — a block also serves the source symbols reached from it: via="callee" is a real 1-hop call edge into non-test code, via="basename" the naming-convention source pair (foo_test.go/foo.go) used only where no call edge landed there. Labelled heuristic="1"; the frame map is unchanged and the innermost frame keeps rank 1, but the hop rows rank in ahead of the remaining frames and the top one's body is served beside it. A non-test trace is unaffected. Unparseable input refuses loudly (never an empty map).
**Try it**
_Map a pasted stack trace onto indexed symbols. CHANGED: in_corpus= now reports the real count (was 0)._
```
$ ./build/ripwire . --from-trace=-
AddressSanitizer:DEADLYSIGNAL
=================================================================
==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0)
#0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:1148
#1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:1174
#2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:5155
#3 0x102e01a44 in main src/main.cpp:5594
#4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)
==41337==ABORTING
```
**Shaped by:** `--top-k`, `--token-budget`, `--help-task`, `--compress`, `--no-redact`, `--plan-lint`, `--run-trace`, `--limit`
**Caveats (stated by the binary):**
- map a stack trace / sanitizer report / compiler-error text ('-'=stdin) onto the indexed symbols: table-driven frame extraction (python / asan / node / compiler / generic), ranked INNERMOST-first over in-corpus frames only (out-of-corpus frames are listed and counted, never ranked).
- The counters close: in_corpus = suspects + merged + unresolved, with one row per file-matched frame no resolver could place.
- --top-k is NOT read here (the frame order is the trace's, not a rank).
### `--run-trace="CMD"`
**Answers:** EXEC-MODE --from-trace — the whole fix-loop entry in ONE call.
Runs CMD under `sh -c` (the make trust model: your user, your environment, stdin=/dev/null, NO sandbox), captures stdout+stderr interleaved, and on a NON-ZERO exit serves the --from-trace bundle for the captured text (frames mapped innermost-first, the innermost in-corpus symbol's FULL body) plus a token-frugal cut of the error / frame-shaped output lines — shown=/relevant=/total= all disclosed, the cut never silent. The command's own exit code is ALWAYS disclosed on ; a command that exits 0 gets a minimal success record (exit, measured duration_ms, a disclosed tail of output) and NO bundle — nothing failed, so there is nothing to map. The record and captured lines are MEASURED (not deterministic, not claimed to be); the MAPPING of the captured text is byte-deterministic, and the document says which part is which. Composes with --token-budget (it bounds the bundle half, like --from-trace); --top-k / --max-tokens are not read here. ripwire's exit: 0 = the command succeeded; 4 = it failed or timed out (the report is on stdout either way); 1 = ripwire itself could not spawn it.
**Try it**
_A command that exits 0: a minimal success record (exit, measured duration, disclosed output tail) and NO bundle — nothing failed, nothing to map._
```
$ ./build/ripwire . --run-trace="true"
```
**Shaped by:** `--top-k`, `--token-budget`, `--run-timeout`
### `--run-timeout=SECONDS`
**Answers:** cap for --run-trace's command (default 600 s;
always disclosed as timeout_s=). A command still running at the cap has its whole process group killed and is reported timed_out="1" — an honest TIMEOUT, never an empty success. Modifies --run-trace only; refused loudly alone.
**Try it**
_--run-timeout alone is refused loudly (it only modifies --run-trace)._
```
$ ./build/ripwire . --run-timeout=5
(empty)
```
**Caveats (stated by the binary):**
- A command still running at the cap has its whole process group killed and is reported timed_out="1" — an honest TIMEOUT, never an empty success.
### `--note-add="TARGET: text"`
**Answers:** pin a field note (write-side memory) to TARGET — a SYMBOL in any spelling the read verbs resolve (bare name, file:name, Scope::name, the canonical id path::scope::name, or @FILE:LINE) or a FILE PATH — in the committed, sorted .ripwire_notes at the repo root.
A symbol is CANONICALISED to its canonical id on write (the id --for/--expand key notes by) and the rewrite is echoed on stderr; a name matching SEVERAL definitions is refused naming each, and a name matching NONE is refused with a did-you-mean. A path target is written even when nothing indexed matches it (a note on a file you are about to add is legal), with a loud stderr warning that it is stored dangling. The date is git's committer clock (HEAD), not wall time, so the line is deterministic; prints the exact written line. Also STAMPS the writing repo's HEAD sha + branch onto the note (a "done"/"fixed" claim is then anchored to the commit it was true at) — a non-git root or an unresolvable HEAD writes the plain unstamped line rather than a wrong sha. MUTATES one file; single-root only. text with no causal/decision marker ("because"/"chose"/"over"/"instead"/etc.) gets a gentle stderr tip toward the decision shape — never a refusal, the add always proceeds.
**Try it**
_Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry._
```
$ ./build/ripwire . --note-add="gitOneLine: which one?"
(empty)
```
**Shaped by:** `--no-redact`, `--legend`
**Caveats (stated by the binary):**
- a name matching SEVERAL definitions is refused naming each, and a name matching NONE is refused with a did-you-mean.
- text with no causal/decision marker ("because"/"chose"/"over"/"instead"/etc.) gets a gentle stderr tip toward the decision shape — never a refusal, the add always proceeds.
### `--notes`
**Answers:** list all field notes grouped by target;
a target with no matching indexed symbol/file is flagged dangling="1" (legal — surfaced nowhere, listed here). Read-only. Notes surface automatically as children on the symbols/files that --for and --expand emit (and the MCP for / fetch_body verbs); the sha/branch attrs appear only on notes stamped by this version, abbreviated (7 hex) for terseness — the full sha lives in .ripwire_notes on disk. An OLDER .ripwire_notes (3 fields, pre-provenance) reads and surfaces exactly as before, with no sha/branch shown. Absent/empty file = zero effect.
**Try it**
_List all field notes (write-side memory) — the committed .ripwire_notes at the repo root, each with the sha/branch it was recorded at._
```
$ ./build/ripwire . --notes
gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]>
```
**Shaped by:** `--no-redact`
### `--pack-task="TASK"`
**Answers:** the budget-shared task bundle: ONE call assembling, under ONE deterministic budget (default 6K tokens;
--token-budget overrides), the whole orientation dance in FIXED order — (1) routed+anchored ranking, (2) top-K full bodies, (3) their 1-hop caller signatures, (4) their field notes, (5) tests_to_run for the top files, emitted in FIXED order ranking>bodies>callers>notes>tests. Each section holds a FIXED, up-front proportional quota of the budget (rank40/body30/caller15/note5/test10, percent); an under-spent section's leftover quota ROLLS FORWARD to the next section, so a small budget still zeroes a section eventually but never past its own fair share. Each section truncates rank-adaptively and the header reports EVERY truncation (no silent caps). A tiny budget degrades to ranking-only WITH the truncation note. Refuses loudly without a task string.
**Try it**
_ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=._
```
$ ./build/ripwire . --pack-task="add a new output format flag to the CLI"
F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexceptverifier N2/N3/N11: the bad-VALUE refusal tablestruct McpValueSpecB1.4: the output-SHAPE members of the list above, as a table rather than a second if-chain. A flag in here selects an ENCODING for rows some verb already produced, so "--json is not supported for Xinline constexpr std::string_view kJsonShapeModifiers[] =
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--top-k`, `--token-budget`, `--for`, `--test-gate`, `--expand`, `--compress`, `--no-redact`, `--partition`
**Caveats (stated by the binary):**
- an under-spent section's leftover quota ROLLS FORWARD to the next section, so a small budget still zeroes a section eventually but never past its own fair share.
- Each section truncates rank-adaptively and the header reports EVERY truncation (no silent caps).
- A tiny budget degrades to ranking-only WITH the truncation note.
### `--partition=N`
**Answers:** (with --pack-task, N=2..16) FAN-OUT form: instead of one bundle, emit ONE shared common core plus N per-agent slices, so N parallel agents stop re-deriving the same orientation.
The task's ranked surface is carved along the call graph's own Louvain communities — a partition is a union of WHOLE modules (largest-first packing) so it reads coherently; when there are fewer modules than agents the widest is cut at its rank median and split="K" says so. The core is exactly the anchors a plain --pack-task would have bodied. --token-budget then means ONE AGENT's budget (core + its partition), not the document's — total_bytes reports the rest. Each inner is byte-identical to a standalone call with that slice, so an orchestrator hands one bundle to one agent verbatim. LIMITS: overlap_mean/overlap_max are pairwise Jaccard over the ids each partition NAMES (window + bodies + their 1-hop neighbors) measured BEFORE budget trimming — a ceiling, not the trimmed truth; and on a task whose surface sits inside one module the split is a rank cut, not a semantic one (read split= and overlap_max before trusting the slices). Refuses loudly without --pack-task, or outside 2..16; --with-graph does not compose with it (N+1 bundles, no single graph — says so on stderr).
**Try it**
_Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities._
```
$ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3
F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX…inline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexceptverifier N2/N3/N11: the bad-VALUE refusal tablestruct McpValueSpecB1.4: the output-SHAPE members of the list above, as a table rather than a second if-chain. A fl…inline constexpr std::string_view kJsonShapeModifiers[] =
... [18 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- LIMITS: overlap_mean/overlap_max are pairwise Jaccard over the ids each partition NAMES (window + bodies + their 1-hop neighbors) measured BEFORE budget trimming — a ceiling, not the trimmed truth;
- and on a task whose surface sits inside one module the split is a rank cut, not a semantic one (read split= and overlap_max before trusting the slices).
- Refuses loudly without --pack-task, or outside 2..16;
### `--with-graph`
**Answers:** (with --for/--pack-task) append a compact MERMAID flowchart of the bundle's top-N (<=8) ranked anchors + their 1-hop call edges among themselves — , right before .
Reuses the --mermaid emitter's syntax. Costs tokens beyond the sigs it sits next to — worth it only when the reading agent renders mermaid natively. Off by default and purely additive: omitted, output is byte-identical.
**Try it**
_Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._
```
$ ./build/ripwire . --for="pagerank power iteration" --with-graph
HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix ent … [line truncated: 42 more bytes on this line]
Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as )What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosureWhat a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph
... [20 more line(s); run it to see the whole thing]
```
**Shaped by:** `--partition`
### `--export=cc.json[:FILE]`
**Answers:** export per-file metrics (loc/symbols/cx/cognitive_cx/fan-in/fan-out/churn) as CodeCharta cc.json (apiVersion 1.3) — write FILE or redirect stdout;
feeds a CodeCharta 3D city
**Try it**
_Per-file metrics as CodeCharta cc.json._
```
$ ./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json
(empty)
```
**Shaped by:** `--legend`
### `--batch=FILE`
**Answers:** one-turn context sweep: FILE ('-'=stdin) is newline-delimited `verb:arg` sub-queries (for/grep/callers/callees/impact/uses/mentions/analyze/lego/owners/cochange/exemplar, path_between:FROM,TO), answered in ONE deduped ;
caps at 16 (over-cap = capped=1). THE SAME `verb:arg` line is accepted by the MCP `batch` verb's queries array (queries=["for:parse the config","callers:escapeXml"]), which also takes the {verb, ...args} object form — one grammar to learn, both front doors.
**Try it**
_One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped ._
```
$ ./build/ripwire . --batch=/aux/batch2.txt
for:incremental cache invalidation
callers:rankGraphTeleport
grep:DEGRADED_PATH_ALERT
lego:Vehicle
```
**Shaped by:** `--no-redact`
**Caveats (stated by the binary):**
- one-turn context sweep: FILE ('-'=stdin) is newline-delimited `verb:arg` sub-queries (for/grep/callers/callees/impact/uses/mentions/analyze/lego/owners/cochange/exemplar, path_between:FROM,TO), answered in ONE deduped ;
- caps at 16 (over-cap = capped=1).
---
## self-diagnosis
### `--doctor`
**Answers:** environment self-check: binary-vs-PATH staleness, grammar tags.scm compile, cache-dir health, git reachability, tree-sitter version, INDEX IDENTITY, and TRACKED-BINARY staleness (a committed binary whose last commit is a git-history ANCESTOR of a same-directory/same-stem source's last commit — never mtime, which a fresh clone stamps at checkout time).
"Dependent source" is a NAMING heuristic (same dir, same filename stem, e.g. tool <-> tool.cpp) — ripwire parses no build system, so a binary built from a differently-named or differently-located source is silently out of scope, neither flagged nor cleared. Single-root only. DIAGNOSTIC, not deterministic (env-dependent by design); exit 0 iff all ok, else 1. Root reports ; each child row carries the BOOLEAN ok="0|1". passed= is the root's count (it was spelled ok= until the vocabulary pass, which collided with the child bool). A FAILING row (ok="0") also carries hint=, the derived verdict (which of self=/which= is stale and the fix, which grammar(s) failed to compile, why the cache dir isn't writable, ...) — a passing row never carries hint=. The index-cache row states the INDEX-VERSION CONTRACT — cache_version=, parser_ver_lean=, parser_ver_rich=, artifact_arch= — the four numbers that decide whether a committed --index-out artifact is reusable at all, and which appear in no other output. lean=/rich= then say whether THIS binary can open the artifact this root would consume (source="auto" its per-root blob, "cache-flag" the file you named, "disabled" under --no-cache), naming WHICH guard refused it: ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame. BOTH families are reported because a team that commits only the lean artifact gets no warm hit on --for/--exemplar/--metrics/--uses. This is a FORMAT verdict about an artifact, never a freshness verdict about an index: every invocation re-validates each file, so a non-ok lean= costs speed, never correctness — the run cold-parses instead. Only an artifact you NAMED with --cache= and this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss, not sickness). The root carries TWO shas, and they answer different questions: at= is the TREE's HEAD (+dirty) right now, built_from= is the commit THIS BINARY was compiled from (byte-identical to --version's own built_from=). They differ from the moment you commit until the next build — normal, so a mismatch is reported, never gated; a stale PATH copy shadowing a fresh build is the binary-path row's job and is decided on inode/mtime/size, not on this sha.
**Try it**
_Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build)._
```
$ ./build/ripwire . --doctor
is one check, its OTHER attributes are check-specific (see help). cache-dir's blobs= is capped at 4096 (kMaxCacheBlobCount); blobs_floor="1" means the cap fired and blobs= is AT LEAST that many, not exactly (absent = the true count); truncated="1" covers that AND an I/O error mid-scan, so blobs_floor= is the narrower, more useful claim when both matter; locks= counts the advisory edit-lock files under locks/ (never unlinked by their holder; the unheld ones older than a day are swept on the next cache write). binary-path compares CONTENT: same_bytes 1 is the copied-install case (ok, copied 1) whatever the mtimes say; on_path 0 fails the row and its hint carries the export line, the state a fresh install is in until PATH is fixed. git's shallow 1 means the clone's history is depth-limited, so every churn number counts only the commits present. volatile= on a row NAMES that row's own attributes that read LIVE machine state — cache-dir scans a per-user directory every ripwire process writes into, so two runs of this deterministic binary legitimately differ in exactly those fields and in nothing else; a determinism comparison strips the named attributes, never the row. tracked-binaries' truncated="1" means the git-history scan was SKIPPED entirely (too many tracked files), so its stale="0" there means unmeasured, never a clean scan. index-cache states the INDEX-VERSION CONTRACT (cache_version/parser_ver_lean/parser_ver_rich/artifact_arch — an artifact is reusable only by a binary carrying all four) and, for the artifact this root would consume, whether THIS binary can open it: lean=/rich= are one of ok | absent | not-regular | unreadable | truncated | not-a-cache | format-version | parser-version | artifact-arch | checksum | corrupt-frame, plus disabled (the no-cache flag: nothing was consulted, which is neither ok nor absent). That is a FORMAT verdict on an artifact, NEVER a freshness verdict on the index: every invocation re-validates each file, so an answer is not stale because lean= is not ok — it is merely slower. source= says whether the artifact was named on the cache= flag or picked automatically, and only a NAMED artifact this binary cannot read is ok="0" (a missing auto blob is the ordinary cold-start miss). NB no flag below is spelled with its leading dashes: an XML comment may not contain a double hyphen, and this legend is one comment. -->
```
**Caveats (stated by the binary):**
- emits fixed repair commands and never prints config contents or shell command lines.
### `--skipped`
**Answers:** WHY the index does not contain a file, and which files it DOES contain but cannot vouch for.
per DROPPED file: why=oversize (limit= names the ceiling — --max-file-size, or the fixed .json/.yaml config ceilings it does not raise), why=excluded (--exclude hit), why=unsupported-ext (ext= has no grammar in this build — the class that hides a whole LANGUAGE). per INDEXED-but-suspect file, nothing dropped: why=degraded-parse (the parse holds ERROR/MISSING nodes — a parser-state fact, never a syntax verdict) and/or why=minified-suspect (ws_freq under 0.070 over the leading 4KB). per unindexed extension — what the map header rolls up as unindexed=. per LANGUAGE this build DID extract from — the mirror of unindexed= (which names what it could NOT read at all); sorted files DESC then name ASC, absent means the language contributed nothing, never a printed zero; files= is a floor (a file with zero extracted symbols is not attributed to any language), symbols= is exact. The root states the ACCOUNTING INVARIANT indexed= + oversize= + excluded= = the enumerated candidate population, plus unsupported_ext=, excluded_dirs= (SUBTREES --exclude pruned: contents UNKNOWN, not zero), pruned_dirs= (SUBTREES this build always prunes by policy — the committed noise/vendor/build denylist and any dir holding a CMakeCache.txt — contents likewise UNKNOWN), degraded_parse=, minified_suspect=, unmeasured= (indexed files this run never parsed) and the effective ceilings, so a zero-row report still states its bounds. rows_capped="1" ⇒ rows are a sample of an exact count. Rows sort by path; composes with --max-file-size/--exclude and multi-root (rows carry the
... [17 more line(s); run it to see the whole thing]
```
**Shaped by:** `--tree`, `--graph-query`, `--external-surface`, `--exercises`, `--community`, `--zoom`, `--pr-context`
**Caveats (stated by the binary):**
- Emit at most N rows, skipping the first M;
- With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0).
- A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing.
### `--exclude=SUBSTR`
**Answers:** drop matching paths (repeatable) --ignore-tests
**Try it**
_Drop matching paths (repeatable) before ranking._
```
$ ./build/ripwire . --exclude=present --exclude=bench --top-k=5
... [6 more line(s); run it to see the whole thing]
```
**Shaped by:** `--skipped`, `--index-out`
### `--map-diff`
**Answers:** the FULL map, re-ranked with a PageRank teleport toward git-changed files (working tree vs HEAD) — changed files and their neighbours float up, but every file can still appear;
this is NOT a filter to only-changed symbols. changed="N" in the header names the seed file count (0 on a clean tree or no-git — teleport degrades to uniform; ranked CONTENT is then identical to the plain default map, but not byte-identical: the map-diff header keeps its changed= and at= stamp). Want only-changed instead? --pr-context.
**Try it**
_Full map re-ranked with teleport toward git-changed files — recorded against a DIRTY tree, so changed= counts the working copy's files and the teleport is live._
```
$ ./build/ripwire . --map-diff --top-k=5
... [7 more line(s); run it to see the whole thing]
```
**Shaped by:** `--json`, `--limit`
**Caveats (stated by the binary):**
- this is NOT a filter to only-changed symbols.
- changed="N" in the header names the seed file count (0 on a clean tree or no-git — teleport degrades to uniform;
### `--cache=PATH`
**Answers:** incremental cache at PATH (re-parse only changed files)
**Try it**
_Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._
```
$ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3
... [4 more line(s); run it to see the whole thing]
```
**Shaped by:** `--doctor`, `--index-out`
### `--index-out=BASE`
**Answers:** CI generate-and-exit: cold-parse the tree and write the committable index artifact, then exit 0 with NO map on stdout.
Writes BOTH families — BASE.lean.ripwirecache (map/ nav/--pr-context) and BASE.rich.ripwirecache (--for/--exemplar/--metrics/--uses are RICH, a lean-only artifact leaves them cold). Consume in a PR job with --cache=BASE.lean.ripwirecache (or .rich.). --exclude shapes the crawl and therefore the blob content. Same-architecture speed cache: consumed on a different arch it self-heals to a full cold parse (correct, slower). NOT byte-identical run-to-run (the header stamps the blob write time); the contract is RESTORE-EQUIVALENCE (a --cache restore == a cold parse), never blob-byte-identity.
**Try it**
_CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout._
```
$ ./build/ripwire . --index-out=/aux/ci_index
(empty)
```
**Shaped by:** `--doctor`, `--legend`
**Caveats (stated by the binary):**
- the contract is RESTORE-EQUIVALENCE (a --cache restore == a cold parse), never blob-byte-identity.
### `--no-cache`
**Answers:** disable the warm-by-default per-root TMPDIR cache (forces a cold parse)
**Try it**
_Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost._
```
$ ./build/ripwire . --no-cache --top-k=3
... [4 more line(s); run it to see the whole thing]
```
**Shaped by:** `--doctor`
### `--no-ignore`
**Answers:** crawl paths the repository's own .gitignore covers.
DEFAULT: in a git work tree the crawl honours git's ignore rules (node_modules/, .venv/, target/, build/, dist/ — whatever the repo declared), and the header discloses ignored_files= / ignored_dirs= when it dropped anything. A non-git root, a missing git binary, or a root that is ITSELF inside an ignored subtree all keep the full walk; --skipped's ignore_mode= says which of the four applied, and rows the ignored set.
**Try it**
_Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied)._
```
$ ./build/ripwire . --no-ignore --top-k=3
... [4 more line(s); run it to see the whole thing]
```
**Caveats (stated by the binary):**
- --skipped's ignore_mode= says which of the four applied, and rows the ignored set.
### `--max-file-size=N[K|M|G]`
**Answers:** skip files larger than N bytes (default 4MB;
raise for repos with big hand-authored source, e.g. --max-file-size=100M; suffix = 1024^n). .json carries a SECOND, fixed 256KB ceiling this flag does not raise (that size of .json is data, not config, and explodes the symbol table); files it drops are counted in the header's skipped_oversize=
**Try it**
_Skip files above a size bound before parsing (note the corpus shrink in the header)._
```
$ ./build/ripwire . --max-file-size=8K --top-k=3
... [4 more line(s); run it to see the whole thing]
```
**Shaped by:** `--skipped`
**Caveats (stated by the binary):**
- skip files larger than N bytes (default 4MB;
- files it drops are counted in the header's skipped_oversize=
### `--refetch`
**Answers:** when the root is a git URL, force a fresh clone instead of reusing the cached one (default: reuse forever;
stderr notes the cached clone's age)
### `--scip=index.scip`
**Answers:** consume a SCIP index as a PRECISION overlay: precise call edges replace name-based guesses (tagged prov="scip"), ambiguous= drops.
Missing/corrupt index → degrades to name-based (never fails). Zero deps (hand-rolled reader).
**Try it**
_SCIP overlay with a missing index REFUSES (exit 1) naming the file — never silently serves the name-based map you named a precision index to improve on (it used to degrade in silence)._
```
$ ./build/ripwire . --scip=does_not_exist.scip --callers=rankGraphTeleport
(empty)
```
**Shaped by:** `--json`, `--pin-census`
**Caveats (stated by the binary):**
- consume a SCIP index as a PRECISION overlay: precise call edges replace name-based guesses (tagged prov="scip"), ambiguous= drops.
- Missing/corrupt index → degrades to name-based (never fails).
### `--pin-census=FILE`
**Answers:** eval-only: write a per-call-site census of WHICH mechanism resolved each call (unique/qualified/receiver-rule/cone/arity/locality/split/scip/binding) and the canonical id of every surviving target — the identity omits.
Under --scip it also writes the index's covered sites, so a precision join needs no protobuf reader. stdout is byte-identical with or without it.
**Try it**
_Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target._
```
$ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3
... [4 more line(s); run it to see the whole thing]
```
### `--mcp`
**Answers:** persistent index server (parse once, many warm queries) over stdio
**Try it**
_initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema._
```
$ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp
{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a symbol; run edit_c … [line truncated: 368 more bytes on this line]
{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 729 more bytes on this line]
{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1095 more bytes on this line]
{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, counts_floor}; calledBy is a FLOO … [line truncated: 797 more bytes on this line]
... [30 more line(s); run it to see the whole thing]
```
**Shaped by:** `--no-stable`, `--no-redact`, `--agent`, `--listen`
### `--listen=HOST:PORT`
**Answers:** serve the MCP server over Streamable HTTP instead of stdio (implies --mcp).
Binds 127.0.0.1 by default (bare PORT = loopback); one listener serves ONE workspace fixed at startup. A non-loopback host (e.g. 0.0.0.0:8080) REQUIRES --mcp-token and refuses to start without it. No TLS — reverse-proxy it.
**Shaped by:** `--no-stable`, `--allow-remote-edits`
**Caveats (stated by the binary):**
- 0.0.0.0:8080) REQUIRES --mcp-token and refuses to start without it.
### `--mcp-token=T`
**Answers:** shared bearer token gating every HTTP request (or set RIPWIRE_MCP_TOKEN);
a missing/wrong token gets a 401. Required for a non-loopback bind.
**Shaped by:** `--listen`
### `--allow-remote-edits`
**Answers:** permit the edit verbs over --listen (refused by default: a remote file-writer is a different trust contract);
forces the token requirement even on loopback
**Caveats (stated by the binary):**
- permit the edit verbs over --listen (refused by default: a remote file-writer is a different trust contract);
### `--eval-stray=FILE`
**Answers:** labelled verdict-accuracy eval for --stray-content: FILE is TSV `refverdict` (merged|superseded|unmerged, '#' comments ok).
Emits per-case want=/got= plus an accuracy, and exits 3 if any labelled case regressed — MEASURE a supersession- threshold change against real labels instead of eyeballing it. A ref absent from the report scores as merged (merged refs are omitted by design).
**Try it**
_Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit._
```
$ ./build/ripwire . --eval-stray=/aux/stray_labels2.tsv
# refverdict labels for --eval-stray (the first three local branches, resolved at capture time; a missing branch is padded with a nonexistent name on purpose)
audit/stranger-install-2026-09-06 merged
backup/rc-C2-preclean unmerged
backup/rc-D-preclean merged
```
### `--eval`
**Answers:** self-eval (co-change recall vs BM25)
**Try it**
_Self-eval: co-change recall vs BM25._
```
$ ./build/ripwire . --eval
ripwire --eval (co-change recovery, averaged over 80 historical commits)
ranker recall@5 recall@10 recall@20
ripwire 2.4% 6.2% 8.8%
BM25 14.0% 18.0% 22.3%
BM25sub 18.7% 22.5% 28.1%
BM25body 32.1% 37.5% 45.1%
fused 8.3% 21.9% 26.7%
anchored 31.2% 38.7% 45.1%
same-dir 3.3% 3.6% 4.7%
random 0.3% 0.6% 1.2% <- floor (random ranking over F=1616 files)
note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not
relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 /
BM25sub / BM25body are QUERY-TIME lexical rankers (whole-name / subtoken /
subtoken+body); fused = RRF(ripwire, BM25sub); anchored = BM25body + anchored PPR
... [5 more line(s); run it to see the whole thing]
```
**Shaped by:** `--legend`
### `--eval-retrieval`
**Answers:** known-item retrieval eval: for symbols WITH a doc-comment, query by NAME and by a doc-comment PHRASE;
reports MRR + recall@1/5/10 per ranker (subtoken+body, name-exact, anchored, routed) per query-mode. Validates query-TIME ranker choice.
**Try it**
_Known-item retrieval eval: MRR + recall@k per ranker per query mode._
```
$ ./build/ripwire . --eval-retrieval
ripwire --eval-retrieval (known-item, 3562 doc-commented symbols; gold is in-corpus by construction)
sample: population=3562 scored=3562 rule=exhaustive (every qualifying symbol; path- and order-independent)
ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize)
ranker query-mode MRR recall@1 recall@5 recall@10
subtoken name 0.724 59.2% 88.9% 93.5%
subtoken doc-phrase 0.931 91.0% 95.3% 96.0%
name-exact name 0.922 85.5% 97.6% 98.3%
name-exact doc-phrase 0.017 0.6% 2.6% 3.4%
anchored name 0.725 60.2% 87.7% 92.1%
anchored doc-phrase 0.926 90.1% 95.0% 95.9%
routed name 0.923 85.5% 97.6% 98.4%
routed doc-phrase 0.930 91.0% 95.2% 95.8%
note: routing chose name-exact on 3560/3562 NAME queries (a NAME query is always identifier-shaped);
the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol
... [4 more line(s); run it to see the whole thing]
```
### `--eval-mined=FILE`
**Answers:** session-trace-mined retrieval eval: consumes a minedpair.jsonl artifact from bench/mine_traces.py (real (query, gold-files) pairs mined from local Claude Code session transcripts) and reports recall@5/10/20 + Acc@k + MRR per arm (for/query/anchor/random), assisted vs unassisted.
### `--eval-skills=FILE`
**Answers:** labelled skill-ROUTING eval: ROOT is a skills directory (one SKILL.md per subdir);
FILE is TSV `promptskill[,skill]|noneprovenance`. Scores deterministic selectors (keyword overlap = the trivial baseline, BM25 over descriptions/full text, name match, the routed --for ranker) on top-1-in- permitted-set plus positive/negative separation (AUC) — does the right skill fire, does every skill stay quiet on off-topic prompts. Ambiguous moments carry a permitted SET; `none` rows are first-class.
**Try it**
_Labelled skill-ROUTING eval over the repo's own skills/ directory (4 hand-labelled prompts)._
```
$ ./build/ripwire skills --eval-skills=/aux/skills_labels2.tsv
orient in an unfamiliar codebase fast ripwire-orient judged
who calls this function and what is the blast radius ripwire-navigate judged
plan parallel worktrees so the lanes do not collide ripwire-change-check judged
what is the weather in Paris none neg
```
**Caveats (stated by the binary):**
- Ambiguous moments carry a permitted SET;
### `-h, --help`
**Answers:** this catalog
**Shaped by:** `--html`, `--color-by`, `--around`, `--callers`, `--callees`, `--affected`, `--expand`, `--metrics`
### `-v, --version`
**Answers:** print the version + short build info, exit 0
**Try it**
_Version + short build info._
```
$ ./build/ripwire --version
ripwire 0.4.0 (dev, AppleClang 21.0.0.21000101, built_from=5ca98897b)
```
**Shaped by:** `--impact`, `--verify`, `--metrics`, `--deps`, `--naming-locals`, `--arch`, `--dry-run`, `--doc-drift`
---
_Generated by `docs/docs_commands_build.py`. See `docs/README.md` for the documentation index._