diff --git a/CHANGELOG.md b/CHANGELOG.md index 312afb08..9ec71177 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,1239 +15,417 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] -### Fixed — the suite's `skip=` count stopped depending on where the checkout lives - -`test/pargates.py` decided whether a gate had SKIPPED — ran, but proved nothing — by looking for the word -SKIP in the first 400 characters of its transcript. Characters, not bytes: the harness decodes the capture -and slices the decoded string, and this suite prints box-drawing rules and em dashes liberally, so every -offset quoted against that threshold has to be in code points too. That is a ruler laid over a document whose origin moves. -Gates open with a banner naming their own absolute paths (`: BIN= ROOT=`) — 515 of the -628 transcripts captured from one full suite run carry the crawl root in their first line — so for those -the window's CONTENTS are a function of the checkout's pathname, and every offset after the banner travels -with it. Measured on `test/w3fixlegendcheck.sh`, whose transcript is byte-identical after line 1: the -banner is 217 B from an 87-character worktree root and 67 B from a 12-character one — a 150 B shift from a -75-character rename, about 2 B per character because the root is spelled twice. The same commit, the same -binary and byte-identical gate output therefore reported `skip=2` from a 137-character checkout and -`skip=3` from a 38-character one, differing only in how one honest arm-level SKIP fell relative to byte -400. That observation belongs to a named tree: commit `3c191bdf` on a feature branch, where -`test/w3fixlegendcheck.sh`'s N=3 partition arm ties (`TIE 0.0928 vs 0.093`) and honestly skips near the -top of its transcript. Re-run on that tree from two checkouts with the same binary and arm output -byte-identical after line 1, the tie row starts at character 302 from a 38-character root and 402 from a -138-character one — it straddles the window by two characters. (In bytes those rows are at 308 and 408; the -six is the box-drawing rule and em dash above them, and the window is in characters.) On the merge base that arm does not tie, so the -symptom cannot be shown there at any path length, and an absolute offset is a property of a tree rather -than of a gate. The suite's summary line is what a contributor reads before every push, so a count that moves with -the pathname is not evidence. - -The exposure was not one gate's, and the dangerous direction was the opposite one. Measured over all 628 -gate transcripts of one full suite run on this repository: 24 gates print a skip MARKER downstream of at -least one absolute-root mention — 28 by the bare substring the old rule actually looked for, the four -extra being gates that only narrate the word — so their classification travelled with the checkout. The nearest was a -REAL standing skip — `test/editchecknotecheck.sh` declares its skip at character 145 — the same figure in -bytes, since everything before it there is ASCII — and 255 more characters of checkout path (a 342-character -root, ordinary for a nested worktree or a CI runner) push that declaration out of the window, at which point a gate that proved nothing is counted as a pass. Which gates -are in range is a property of the machine rather than of the commit, so a wider window was never the -answer. - -The rule is now written down instead of measured in bytes: **a gate that proves nothing says so before it -claims anything.** The first verdict marker in the transcript decides — a SKIP ahead of every PASS and -FAIL marker is a whole-gate skip, while a SKIP that follows one is an arm-level skip inside a gate that -did prove something, and that gate is a pass. It reads verdict markers rather than a bare substring, -because five gates narrate the word SKIPPED in prose and prove plenty. This is what the tree already did -on purpose — `test/namingcalibrationcheck.sh` runs its live arm first so its skip banner precedes its -instrument arm's pass rows, and `test/argvdiffcheck.sh`'s skip is its opening line — so the classification -is unchanged where it was already right: replayed over those same 628 transcripts, the new rule and the -old one disagree on ZERO gates, and a full suite run reports the same three environmental skips as before. - -The same failure family turned up one level down, inside the fix, and the review caught it. The rule -counts a FAIL as a verdict, but the shared failure-marker expression was compiled without `re.M` and the -classifier matches it against a WHOLE transcript — so `^` bound only to the start of the string, every -anchored alternative in it was dead below line 1, and the only one that could still fire was the single -unanchored one. A gate that printed a FAIL row, then a SKIP row, and exited 0 therefore read as having -proved nothing, when it had claimed a verdict before it skipped. It is now compiled with `re.M`, which is a -no-op for the other caller: that one searches a line at a time, and a single line has no newline for `^` to -find. The arm that pins it, (D2), deliberately does NOT print the one unanchored alternative — a probe -carrying it would be matched by accident and the arm would pass while asserting nothing, which is the -difference between a test and a demonstration. - -One direction is newly open and is disclosed rather than left to be discovered. A whole-gate skip that -prints any PASS row BEFORE its skip marker now counts as a pass, because in a transcript it is -indistinguishable from a gate that proved an arm and then skipped one. No gate in the suite does this -today — the replay above is the evidence — and it is the convention both sanctioned skips already follow, -but nothing enforces it: a gate that grew a fixture-present PASS row above its skip banner would go from -skip to pass silently. Enforcing it needs a static sweep of every gate's skip path, which belongs beside -`test/gateexitcheck.sh` arm (D); that arm flags an `exit 0` only where a skip word and `ALL PASS` appear -within three lines of each other, so it does not police marker order and never did. - -`test/skipclassifycheck.sh` is the gate, and it drives the real `test/pargates.py` rather than a -reimplementation of it: one probe gate, byte-identical, classified from two corpus roots about 130 -characters apart, after a presence guard proves that the same probe's skip row really does land on -opposite sides of the old boundary — without that contrast the arm would pass against a classifier that -never read the output at all. It reds on the byte-window classifier at four arms and greens on the rule. -The gate side of the same contract is `test/gateexitcheck.sh` arm (D). - -### Fixed — a relative command with no anchor, and the roots that never declared themselves - -A second review of the three `--situ` entries below found twelve defects — counted one per -independently described correction below, which is four surfaces that spelled a path or a command -relative to a root they never declared, one in the shared path relativizer, three in the new -lexical-siblings block and four disclosure readings dropped when sentences became attributes (4 + 1 + 3 -+ 4) — every one of them a document that could not be resolved by the reader holding it, and all twelve -are fixed here. The two byte ratchets named at the end moved with the fixes and are not counted among -them: a pin is not a defect. A third review, of this entry's own fixes rather than of the entries below -it, found two more — the filesystem root in that same relativizer, and a quadratic scan in the new -lexical-siblings block — and both are fixed here too. A fourth review found a FIFTEENTH in shipped -output: a `run=` command did not shell-quote its path. Separately, this lane's own new cap left two -published counts stale (README's cap total and `docs/TUNING.md`); both were republished by their -generators rather than edited, and they are drift this entry caused rather than a sixteenth defect. - -A run= IS A COMMAND, AND ITS PATH COMES FROM THE CORPUS. `testmap.h`'s `spell()` concatenated the -runner verb and the path, so a repository containing the legal filename `test/check;touch PWNED.sh` -made the tool emit `run="bash test/check;touch PWNED.sh"` — a command this tool hands an agent to -paste, which runs `touch PWNED.sh` in the reader's shell (CWE-78, external reachability). The path is -now always one shell argument. It is quoted only when it is not provably safe, and that is measured -rather than preferred: `shSingleQuote` always wraps, so quoting unconditionally would move the `run=` -bytes of every test row in eight emitters — 13 literal command assertions across 7 gates, -`docs/COMMANDS.md`, 15 committed capture snapshots, README and the `printf_parity` pins. Every path -`git ls-files` tracks here is inside the safe allowlist (`[A-Za-z0-9._/-]`, never a leading `-`, which -a shell reads as a flag), measured at 0 outside it, so the conditional form is byte-identical on every -real corpus while a hostile name is still quoted — `printffmtparitycheck` needed no re-pin. The -predicate is an allowlist, so an unenumerated byte is quoted by default. `test/runhintcheck.sh` arm (5) -EXECUTES the emitted command in a scratch corpus and asserts the payload did not fire, with the -unquoted spelling as its control. Red before the fix: the emitted command created the sentinel file. - -AND QUOTING WAS NOT SUFFICIENT — the same trust boundary, one layer in. A root-level -`-cimport os;open("PWNED","w")#_test.py` passes `isTestPath`, keeps its leading dash, survives quoting -intact, and then `python3` reads `-c` as "execute this code": the path reaches the INTERPRETER as an -option rather than the shell as code. Measured, both directions, over the whole population of verbs -`runnerVerb` can emit (exactly two — `.sh`→`bash`, `.py`→`python3`): `python3 ''` exited 0 and -created the payload file, `python3 -- ''` exited the file's own 7 and did nothing, and -`bash -- ''` likewise. `bash` did not reproduce the bypass with the equivalent payload (it -rejected the combined `-c` form, rc=1), so the confirmed case is `python3`; `--` is emitted for both -because both honour it. An option terminator now precedes any path that is not provably safe — a -leading `-` is already outside the allowlist, so this too is byte-identical on every real corpus and -`printffmtparitycheck` still needs no re-pin. `runhintcheck` arm (6) pins it, with the -quoted-but-unterminated spelling as the control that proves quoting alone was not the fix. - -A RELATIVE COMMAND IS ONLY AS GOOD AS ITS ANCHOR. Making `run=` root-relative is what makes a change -report independent of where the tree is checked out — and it makes every one of those commands useless -to a reader who cannot tell what they are relative to. Four surfaces had exactly that hole. The shared -run-hint clause claimed "relative to root=" unconditionally, including on a MULTI-root run, which -declares no `root=` at all and (correctly) keeps the absolute command: the spelling and the sentence now -answer to one predicate, `testmap.h runsAreRootRelative`, so they cannot disagree. `--flags --flip` -spelled every `p=` relative to the crawl root and declared no root either; `` now carries `root=` -with the one sentence that defines it, like every other verb. The MCP edit receipt — the surface that -hands a caller a command to paste — spelled `file`, every `tests_to_run[].run` and its stderr `next:` -relative to a root it never named; it now carries `"root"`, single-root only, the same condition every -other `root=` keeps. And `--help` still told the reader `run=` was "spelled with the same root you -scanned", which stopped being true in this lane; `--help` and `docs/COMMANDS.md` now say what the code -does. - -A "./" THAT RETURNED TOO EARLY. `sarif.h rootRelativeUri` stripped a stored path's leading `./` and -RETURNED, before the root prefix was ever tried. That is right for the root `.` and wrong for every -other relative spelling: `ripwire ./corp` stores `./corp/test/x.sh`, the early return yielded -`corp/test/x.sh`, and pasting that from the root the document declares is `cd ./corp && bash -corp/test/x.sh` — rc 127. Both sides now drop the optional `./` first and compare what is left, which -leaves the one case the early return got right byte-identical. `test/rootrelemitcheck.sh` ARM 9b turns -the old spelling pair into a matrix: `.`, `corp`, `./corp`, `corp/`, an absolute path and a symlink all -print the SAME command, and each printed command is EXECUTED from the root it names. - -AND THE ONE ROOT THAT IS ITS OWN SEPARATOR. That same relativizer then matched a prefix only when the -byte after it was a `/`, which the filesystem root can never satisfy: under `ripwire /` the stored -spelling is `/test/check.sh`, the byte after the prefix is `t`, and the ABSOLUTE path was emitted into a -document whose `root="/"` declares every path relative to it — `testmap.h runsAreRootRelative` is true -for any single non-empty root, `/` included, so the envelope's claim and its own rows disagreed. This is -every `p=`/`uri=` emitter in the tool, not SARIF alone: all of them route through this one pair. The -added clause strips the single leading slash, runs AFTER the general shape so no other prefix changes by -a byte, and is guarded on length so a file spelled `/` stays `/` rather than becoming an empty URI. A -corpus at the filesystem root means crawling the whole machine, so no end-to-end arm can reach it; -`test/sarifcheck.sh` arm 11 is a unit driver over the function itself, compiled with the flags CMake -gave the binary under test (the recipe `extentcheck.sh` (U) and `jsonwalkcheck.sh` already use). Its 22 -rows pin both halves together — the predicate the envelope claims and the URI the relativizer returns — -and the non-root prefixes sit in the same table, so an over-stripping fix fails there rather than on a -consumer's machine. Red before the change: 4 of the 22 failed, including `rootRelativeUri( -"/test/check.sh", "/" )` returning `/test/check.sh`. - -A SMALL BLOCK PAGED WITH SOMEONE ELSE'S WINDOW. The new lexical-siblings block honoured `page.offset` — -which is section `[1]`'s blast-radius offset. `--situ=F --offset=20` printed `shown=0 total=9 capped=1` -with a `next:` offering `--limit=9`, relief that cannot restore rows an OFFSET removed, and `--offset=7` -dropped six rows silently. It is a small fixed block with a cap, like the decl/def partner rows above it: -cap and `--limit`, no offset. In the same family, `unindexed_rows_floor` was computed only for a NON-EMPTY -list and the emitter suppressed the empty one, so the case where the crawl's 500-row cut removed the only -candidate printed nothing at all — a silent zero, which is the one thing METHODOLOGY §9 forbids outright. -The floor is a property of the candidate list, not of the answer: it is recorded whenever that list was -short, and the block speaks at zero. The MCP twin's `siblings_total` was the length of the array beside it -— a tautology — and now states `siblings_capped` explicitly beside a population. - -A CAP THAT BOUNDED THE ANSWER AND NOT THE WORK. That same lexical-siblings block compared every -unchanged indexed file and every unsupported crawl row against every changed path, and -`isLexicalSiblingOf` re-split both paths into directory and stem on each pair; the sibling ROW cap -applies only after collection, so it bounded what was printed and never what was computed — -O( (F + U) x C ). SAME DIRECTORY is the rule's most selective clause, so the changed paths are now -indexed by directory once (a sorted vector and a `lower_bound`, not a hash or tree map) and a candidate -is compared only against the changed paths sharing its directory: one `dirOf` and one binary search per -candidate, nothing more for a candidate whose directory nothing changed in. The predicate is still -`isLexicalSiblingOf`, called on the narrowed range rather than restated, so the rule cannot drift from -the prose that documents it. METHOD: the two implementations are timed on the function itself — its only -inputs are `ing.files` and the changed-file bitmap — built `-O2` with the flags CMake gives the shipped -binary, the two binaries interleaved, best of 5, two passes. The path population is a real -`llvm-project` checkout (`4d5358b1`, clang+llvm, 8,856 paths) and `golang/go` (12,555 paths), plus that -llvm population grown to the 182,555-file rung of `docs/EVALS.md` by re-rooting whole copies of the tree -— synthetic in SIZE only, every path keeping a real directory shape and a real stem. The changed set is -spread evenly rather than clustered, because a clustered one makes every candidate's FIRST comparison hit -and understates the old cost. The host was at load average 38 on 18 cores (other work in flight), so the -absolute figures are upper bounds and the interleaved RATIO is the measurement: at 182,555 files C=500 -2.20 s → 9.7 ms (226x) and C=2,000 9.10 s → 22.6 ms (403x); at the real 8,856-file rung C=2,000 333 ms → -11.6 ms (29x); on `golang/go` C=2,000 449 ms → 42.8 ms (10x). The rows are unchanged and proven so: the -emitted row list is byte-identical between the two implementations on all nine rungs measured (1,413 rows -in total), and `test/situshapecheck.sh` arm (7) already pins the case this narrowing could break — a -same-stem DECOY in another directory stays excluded, beside the same-directory-different-stem row, the -test partner and the changed file itself. No timing gate is added; there are none here. - -AN ATTRIBUTE WITHOUT A READING IS A TOKEN, NOT A DISCLOSURE. The compression below shortened four -sentences into attributes, and four readings went with them: what makes the counts a floor (call edges are -name-based), what an unindexed file IS, which header the resolver gauges come from, and whose cap -`prcontext_cap=` is. `--situ` is the one dialect with no legend anywhere to look a name up in — it refuses -`--legend=compact` — so each gauge keeps a short gloss, and `test/situshapecheck.sh` arm (8) asserts the -READING, not the token. Two byte ratchets moved with them (floor 200 → 360, partner 140 → 220): a ratchet -that forbids a restored disclosure is a ratchet aimed at the wrong thing. - -Measured with `wc -c`, this lane's base binary (`6621370f`) against this one over the SAME tree, so the -pair carries all three `--situ` entries below together. On this repo (root 131 chars): -`--situ=src/graph.h` 4,448 → 2,955 B, `--situ=src/situ.h` 2,332 → 2,040 B, `--situ=src/testmap.h` -2,325 → 2,033 B, `--test-gate=src/testmap.h` 5,455 → 5,247 B. On RocksDB @0e2801ac (root 66 chars): -`--situ=db/write_batch.cc` 7,489 → 7,376 B, `--test-gate=db/write_batch.cc` 9,946 → 9,868 B, -`--affected=db/write_batch.cc` 7,124 → 7,113 B. Gates: `test/situshapecheck.sh` (17 rows red on that base -binary, arms (8)–(11) new), `test/rootrelemitcheck.sh` ARM 9b/9c/9d, `test/runhintcheck.sh` 2c/2d, -`test/receiptpostcheck.sh` (18). Two gate self-checks were wrong in the same way the code was — an empty -`run=` made `eval ""` succeed, and an empty `next=` fell out of an if/elif chain printing neither PASS nor -FAIL — so each now reds on the outcome it exists to forbid. `situStemOf` was a fourth spelling of -`stripExt( baseNameOf( p ) )`; one `mention.h pathStem` now serves all four call sites. Pins moved: -`test/testgatelegendbudgetcheck.sh` 3,000 → 3,070 B for the 56 B conditional root sentence (measured -2,957 → 3,013 B on its `src/model.h` fixture), and `test/printf_parity.manifest` for `pack_task` and -`help_all`, the two labels whose text this round changed. The cap inventory is 211, not the 210 the -reference-guide entry below records: this PR's own sibling-row cap is the 211th, and `README.md` and -`docs/TUNING.md` were regenerated to say so (`test/readmedriftcheck.sh` (L2), `test/capsweepcheck.sh` (C)). - -### Added — `--situ` lists a changed file's lexical siblings - -The files that move WITH a changed file are usually its neighbours by name, and the caller walk can reach -none of them: a header does not call the source that implements it, an `.inl` is not indexed by any grammar -in any build, and a harness the graph cannot link — a fixture-built test, a generated `main` — is reached by -nothing. A byte-and-answer attribution over a frozen 30-question set found two answers incomplete for -exactly that reason. Section `[1]` of `--situ` now lists them, under the decl/def partners and the floor -clause: `lexical siblings (N) not_dependents=1 — same directory and stem as a changed file (its header/impl -partner, its test, its .inl): NOT transitive dependents, so they are absent from the list below; lexical and -static, never a graph result`, then one root-relative path per row. The rule is the -dumbest one that is always right — same directory, and the same filename stem or the stem-partner convention -the tests-to-run rows already use (`_test`, `test_`, `Test`, `_unittest`, `_spec`). Same -directory is load-bearing rather than a speed trick: a same-stem file in another directory is a namesake, not -a partner, and listing namesakes would make the block noise on exactly the large trees it is for. The -candidate population is the CRAWL's, not the index's, so an `.inl`/`.ipp`/`.tcc` partner — the sibling a C++ -change most often has to edit, and one no grammar can read — is named; the crawl's unsupported-extension row -list is itself capped, and the one case where that can shorten this list is disclosed as -`unindexed_rows_floor=1` — on the EMPTY list too, because a cut that removes the only candidate is exactly -the case a silent zero would hide. The block is capped at 8 rows with `shown=`/`total=`/`capped=1` and a -pasteable `next:`, and `--limit=N` raises it like the report's other two listings; it does NOT take section -`[1]`'s `--offset`, which is the blast-radius window's, so no offset can empty it. The MCP -`situational_awareness` twin carries the same list as `siblings` with `siblings_total`, `siblings_capped` -(always emitted: that payload serves every row, and an absent flag would be the silence this rule forbids) -and `siblings_unindexed_rows_floor`. It costs what it lists: the block's own rendered lines on RocksDB -@0e2801ac at `--situ=db/write_batch.cc` are 276 B — a 244 B header and one 30 B row naming -`db/write_batch_test.cc`, which no other section of that report reaches; on this repo, where every source -file is a lone `.h`, no file has a lexical sibling and the block prints nothing at all. Gate: -`test/situshapecheck.sh` arms (7)–(7d) on a fixture with a `.h`/`.cc`/`_test.cc`/`.inl` quadruple, a -same-stem DECOY in another directory and a same-directory file with a different stem — both must be absent — -plus a nine-sibling stem for the cap and its disclosure, a no-git copy of the same tree proving the block is -static (and therefore cannot leak), and the MCP twin agreeing row for row. All four arms red on the previous -binary. - -### Changed — `--situ`'s disclosures are attributes - -`--situ` is the only report with no XML root to hang attributes on, so every disclosure it owed was written -as a sentence, and the sentences grew: the graph-count floor clause ran 601 B, the decl/def partner header -228 B, the tests-to-run header 233 B and the script-gate caveat 167 B — 1,229 B of prose on every call, -carrying facts a reader can only act on once they are named. They are now named, and the four lines together -are 905 B. The floor line is `counts_floor=1 graph_ambiguous=N graph_unresolved=N graph_unindexed=N (the map -header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic -dispatch, callbacks and macros can be missing; a zero is "none found", never "none exists"` (601 → 344 B), -using the same attribute spellings the XML and JSON dialects already use, so the three share one vocabulary. -The partner header carries `not_dependents=1` (228 → 209 B), section `[1]` carries `prcontext_cap=20` where -it used to spell `--pr-context`'s own cap as an aside, section `[2]` carries `order=evidence` — the attribute -`--affected`'s root already carries for the same ordering (233 → 220 B) — and the script-gate blind spot is -`script_gates_unmodelled=N`, the same counter `--affected` publishes, with its cause kept (167 → 132 B). An -attribute is shorter than a sentence; it is not shorter than the FACT, so every gauge keeps a short gloss — -this is the one dialect with no legend anywhere to look a name up in (`--situ` refuses `--legend=compact`). -Nothing was dropped: every floor, cap and caveat survives, and the readings that have no attribute form (how -to read a zero; what `[changed]`/`[partner]`/`hops` mean on a row) stay as the shortest sentence that defines -them. The four line lengths above are the gate's own `${#line}`, measured on this repo at -`--situ=src/graph.h` (the partner header on the gate's fixture at `--situ=core/widget.cc`, since this repo -has no decl/def partner for `graph.h`), against the binary this lane branched from (`6621370f`) over the same -tree — one number, one corpus, and the gate's header carries the same table. The whole-report numbers for -this lane are in the review entry above, where they belong: the same pair of binaries also carries the -relativized `run=` and the new sibling block, so no single entry owns them. The gate is the new -`test/situshapecheck.sh`: one arm per converted disclosure, each -asserting the attribute is present, that its value agrees with the XML sibling's where one exists -(`graph_unindexed=`, `script_gates_unmodelled=`), that the reading survives, and a per-line byte ratchet so -the prose cannot creep back; 10 of its rows are red on the previous binary. `test/floormarkcheck.sh` keeps -the two anchor phrases it matches — `counts_floor=1` and "is a FLOOR, never a total" — and situshapecheck -mirrors them, so a regression reds in both. - -### Changed — one absolute root per change report - -`--test-gate`, `--situ` and `--affected` state the crawl root once, in the envelope (`root=` in XML and -JSON, the `root:` line in `--situ`'s text), and every path below it is relative to that root — which is -what makes the document independent of where the tree is checked out. One emitter never joined: the -`run=` command. It pasted the stored disk path verbatim, so on an absolute root `--test-gate` printed -the checkout prefix three times (the anchor, `next=`, and every `` row's `run=`) and `--situ` once per -runnable test line: a per-row cost against a per-document fact. The runner index now takes the run's root -and spells the command through the same relativizer every `p=` beside it uses, at all fourteen sites that -build one, so the twelve emitters sharing it cannot disagree; a multi-root run, whose disk path lies under -no single root, keeps the absolute command rather than become relative to a root that does not contain it. -The rule is stated where it is consumed: the shared run-hint clause gains "A run= command is relative to -root=: run it from there." (56 B, emitted only on a document that has rows AND a single root — a multi-root -run declares no `root=` and keeps the absolute command, so the sentence would be a false claim there) and -`--situ`'s `[2]` header says "a (run: …) is relative to root:". The saving is one root spelling per echo -less that clause, so it grows with checkout depth and with how many rows have a runner at all; the -whole-report numbers for this lane are in the review entry above, measured against the binary it branched -from over one tree. The gate is a new -ARM 9 in `test/rootrelemitcheck.sh`: a fixture carrying a real runner script, at two checkout depths, over -the eight verbs that echo a command — one anchor per document, no absolute path anywhere else, -byte-identical documents at both depths, and the printed `run=` actually executed from the declared root. -Red first on the unchanged binary (8 FAIL rows); `test/runhintcheck.sh`'s pins move with the contract, and -`test/printf_parity.manifest` moves for `--pack-task` alone, the one verb whose legend text changed. - -### Added — `--in=DIR` scopes the recent-changes block to a directory and stubs the map it was not asked for - -"What changed recently in DIR?" is six of the thirty questions in this project's frozen reference set (the -RocksDB corpus pinned at `0e2801ac`, scored by the frozen-30 harness described in `docs/EVALS.md`), and -`--rank-by=churn-decay` answered it with a whole-repository symbol map plus one global `` -block that a directory with more than 40 recently-touched files never fits into; nothing in the binary took -a directory as a scope. `--in=DIR` (root-relative, an existing directory under the root; a trailing slash is -ignored, absolute paths and `..` are refused) keeps the global block byte-identical — three of the six golds -sit outside the named directory and complete only through it — and adds a second block -`` after it with DIR's files only, `p=` spelled root-relative exactly -as the global block spells them, same order. That spelling is the whole of the retrieval result: scored by -the same harness on the same 30 questions, the sub-root-relative spelling the obvious workaround produces -completed 0 of 30 against 19 of 30 for the root-prefixed one, because every gold path in the set is written -root-relative. - -The block pages the way every listing here pages, in `pageview.h`'s vocabulary: 40 rows by default, then -`capped=` beside its `n=` on every page (rule 3), `has_more=`/`next_offset=`/`offset=`/`limit=` when the -listing was cut or a window was asked for, and a pasteable `next=`. Its `of=` IS its total, so the paging -half carries no `total=`: one number under two names is a shape this project has removed elsewhere, because a -parser then has to know they are the same listing to avoid counting it twice. `next=` replays THIS -run's own corpus and window flags (`--since`, `--exclude`, `--no-ignore`, `--ignore-tests`), so the page it -names is a page of the same answer; a presentation flag is deliberately not replayed, because it cannot move -`of=`. Past 120 bytes (`kNextAttrMaxBytes`, the ceiling every other `next=` in the tool already respected) -the attribute is absent and `has_more=` still says the page exists — a hint that pastes wrong is worse than -none. The replayed set is ENUMERATED against the flags that can ride beside `--in`, not hand-picked: it also -carries `--max-file-size` when that run set one, because the crawl's size ceiling drops files out of the index -and the rows are indexed files (a hint emitted under `--max-file-size=2K` named a page of `of="3"` where the -run that emitted it saw `of="2"`), and `scopedMapNextInvocation` now lists every other rideable flag with the -reason it is NOT replayed — a cache flag indexes the same files, `--refetch` would fetch a newer tree, -`--scip` moves edges and not files, and the presentation flags cannot move `of=`. The scoped block rides -exactly when the global one does: an absent block means no history was mined, `n="0"` means history was mined -and no file under DIR was touched. That is a propagated FACT (`DecayedChurnMined::anyHistory`, carried through -`ChurnRanking` to the serializer) rather than a reading of the rows: a window whose only commit touched no -indexed file — the commit that deletes a file is the smallest one — has zero rows AND a mined history, and -inferring the second from the first published it as "no history was mined". - -The symbol map collapses to a disclosed stub `` — the map was -not asked for, so it is not ranked at all (no PageRank runs, and the header carries no `pr_iters=` for an -iteration that did not happen), and `would_show=` is that same run's own `shown=`: the symbol DEFINITIONS it -ranks, counted individually exactly as `shown=` counts them. The rows that run prints FOLLOW from the identity -the map legend already publishes for `shown=` — `rows + sum(overloads-1) = shown`, since the print loop -collapses a const/non-const overload pair into one row carrying `overloads="2"` (this repository: `shown="200"` -over 193 rows, 7 of them at `overloads="2"`). The first wording said "how many symbol ROWS", which is a number -the document does not contain. Reporting post-collapse rows there instead is not available at any price worth -paying: WHICH definitions survive the top-K cut is a fact about the ranking, and not ranking is the whole point -of the stub. It is named as the definition count it is rather than hedged as a ceiling, because a floor/ceiling -marker in this tool means "we could not see everything" — `counts_floor="1"`, `_capped`, the truncation -disclosures — while `would_show` is EXACT and only its UNIT differs from a reader's guess; spending an -uncertainty marker on a unit difference would make "ceiling" mean "exact, but not in the unit you assumed" and -weaken every honest use of the word elsewhere in the output. It deliberately borrows no paging attribute: `total=` is reserved for THE total (rule 2) and the stub's -number is a page size, `shown=` would drag a `capped=` with it (rule 3), and rule 3's own sentence sanctions -an element that carries neither. - -DIR is validated against the CRAWL and not only the filesystem — at least one indexed file must be spelled -`DIR/`, byte-exact. The filesystem answers a different question: on a case-folding volume `--in=DB` is a -directory, a symlink alias is a directory, and a subtree `--exclude` dropped is a directory, and all three -would otherwise be answered with an empty block, which reads as "nothing changed there". - -REFUSED, exactly, and `--help` lists the same set: with any flag that answers instead of the scoped map, -under multi-root, with `--top-k=N` for any N (the map it sizes is the stub), and with `--json`. The first of -those is DERIVED from the flag tables rather than from a list of verbs — the shape `--html` already used — -so `--map-diff`, `--expand`/`--outline`/`--pack-signatures`, `--doctor`, `--batch`, `--mcp` and the CLI edit -bridge are covered by the same three lines that cover a report verb, and a flag added tomorrow refuses -tomorrow with nobody editing the guard. `--in` is NOT a member of the paging verb set: it is a modifier of -the default map, and membership made the shaping guard refuse every `--top-k`/`--max-tokens`/`--token-budget` -beside it with a message naming verbs and claiming the default map honours the budgets it had just refused. -`--limit`/`--offset` compose (they window the scoped element) and so do `--max-tokens`/`--token-budget` (they -shape the document that is emitted). The MCP surface exposes no churn ranker, so there is no twin to extend. - -Two flags are refused for a DIFFERENT reason and now say so, and WHICH two is derived. `kMapShapingFlags` is -the tool's own list of flags that shape the bare map without selecting a verb, so that table minus the -ride-along table is exactly the residue a scoped run cannot compose with: `--no-redact`, `--metrics` and -`--map-diff`. `--map-diff` is the one that genuinely answers instead — it takes its own ranking branch ahead of -churn-decay, so no scoped block was ever going to be built — while the other two shape or un-redact a map this -run replaces with the counted stub. `--metrics` decorates symbol rows the stub does not print, and it is -refused as inert by table membership rather than by a hand-written case, so a shaping flag added tomorrow with -no ride-along row gets the right sentence without anyone editing the guard. `--no-redact` selects no -operation — it only stops -body redaction — and a scoped run serves no bodies at all, because the symbol map is the counted stub. So the -derived "answers instead" diagnostic stated a reason that was not this run's: the flag is INERT here, not -overridden. It is refused ahead of that diagnostic, in the shape the bare map already uses for the same flag, -pointing at both ways forward (drop it, or pass it to a body-serving verb). It is deliberately NOT added to the -ride-along table: accepting an inert modifier silently is the other half of the same defect. The class was then measured — and the first -measurement of it, published in this entry, was wrong. It said that of the flags the guard walks every one -probed reaches its own pairing refusal before this line, with `--external-surface` the only other flag -arriving here. Swept over the derived flag universe (`test/flaguniverse.py`) rather than a sample, 119 of the -171 `kBoolFlags`/`kViewFlags` rows reach that line, and 43 of those answer when run alone — so neither -"reaches the generic line" nor "answers alone" separates the class, because `--metrics` answers alone and what -it answers IS the default map, decorated. Table membership separates it, which is why the refusal derives the -set and `test/recentscopecheck.sh` arm 6s2e re-derives the same set from the same two tables on every run. -`--external-surface` remains a correct competitor and stays the control arm. The guard on the new branch is `--in` AND `--no-redact`: written without the first -half it refused every `--no-redact` run in the tool while quoting `--in=DIR` at it, which eight gates said in -one suite and no arm added for the fix could, since every one of them passes `--in`. - -Measured on the RocksDB corpus at `0e2801ac` (`--rank-by=churn-decay`, warm cache, bytes on stdout via -`wc -c`): 39,942 B bare → 10,601 B with `--in=db`, 10,525 B with `--in=util`, 11,071 B with `--in=table`. -The saving is the stub (69 B in place of the 200-row map); the scoped block itself costs ~2.3–2.8 KB per -answer, and the global block is unchanged. On this repository's own tree: 46,787 B → 9,629 B with -`--in=src`; on llvm-project (183,835 tracked files) 48,150 B → 6,051 B with `--in=llvm/lib/Analysis`. -Because the map is never ranked, sorted, bucketed or estimated under `--in`, the run is also cheaper, though -only by the share of it that ranking was: user time, median of five interleaved warm samples with a scratch -cache, 0.73 s → 0.71 s on RocksDB and 2.55 s → 2.37 s on llvm-project (~3% and ~7%). Ingest and the call -graph dominate both, and that is the honest size of this win. - -Gate: `test/recentscopecheck.sh`, 99 arms on a 53-commit fixture with 45 files under `db/` (plus two -fixtures of its own for the corpus arms) — the scoped rows -are only DIR's and spelled as the global block spells them, the global block is byte-identical with and -without the flag, page 2 (`--offset=40`) is the exact remainder with no overlap and the pasted `next=` -reproduces it byte-for-byte, `next=` replays `--exclude` and the pasted page lands on the same `of=`, an -over-120-byte `next=` is absent while `has_more="1"` remains, the stub carries no `total=`/`shown=`/`capped=` -and no `pr_iters=` rides the stubbed header, a window that mined nothing prints NEITHER block, a case-folded -name / a symlink alias / an excluded subtree each refuse naming the crawl, eight preemption arms sample the -derived refusal (`--lint`, `--hotspots`, `--query`, `--map-diff`, `--expand`, `--pack-signatures`, `--doctor`, -`--batch`), `--top-k` refuses with exactly one message where it used to print three, and `--max-tokens`/ -`--token-budget`/`--limit` compose with a clean stderr. Two arms cover the continuation's corpus directly: -a run under `--max-file-size=2K` on a fixture holding one oversize file must replay the ceiling and the pasted -page must land on the same `of=`, and a window whose only commit touched no indexed file must print `n="0"` -where `--since=HEAD` (which reads no commit) still prints neither block. `perl` and `xmllint` are -PREREQUISITES of the gate (exit 2, naming the tool) rather than arms: a missing tool is an environmental -condition, and reporting it as a FAIL made `test/regression.sh` name this gate as a product regression for a -tool the machine never had. Three arms cover the refusal wording: the inert `--no-redact` message, the -`--external-surface` control that must keep the competing wording, and — the one that was missing — a -`--no-redact` run with NO `--in` at all, which fails if the message so much as mentions the scoped flag. - -### Fixed — two generated documents published numbers and links nothing derived - -`docs/COMMANDS.md`'s table of contents is generated: one `[`--flag`](#anchor)` per entry, with the anchor -derived from the flag's spec. The derivation replaced every run of non-alphanumeric characters with a hyphen -and trimmed the ends (the `--in=DIR` heading became `#in-dir`), where the renderer's rule DELETES that -punctuation instead of substituting it — the anchor it mints keeps the flag's own two leading dashes and loses -the `=`. All 169 links in the document therefore resolved to nothing, and had done since it was first -generated; markdownlint's MD051 had been reporting it 28 times on a single line. Fixed in the -generator (`docs/docs_commands_build.py`), which now states the renderer's own rule — lower-case, drop every -character that is not a word character, a hyphen or a space, then spaces to hyphens — and assigns anchors in -emission order so a repeated heading would get the `-1` the renderer appends rather than two links to the -first. Gate: `test/docscommandscheck.sh` arm (J), which audits every fragment against the headings (fenced -sample output skipped — a `###` line inside a code block mints no anchor) and restates the renderer's rule -instead of importing the generator's, because a gate that asks the generator what the anchor should be agrees -with the generator's mistake. Arms (A)–(I) were all green throughout: (B) compares flag NAME sets and (G) -compares bytes, and a document can be byte-reproducible with every link in it dead. - -`docs/TUNING.md`, likewise generated, asserted a sum instead of deriving one: "`112 + 12` accounts for the 128 -NAMES" is 124, four short of the distinct-name count in the table two lines above it — in the one paragraph -whose subject is that quoting a wrong pair "would be wrong in both halves at once". Recounted from the same -data the table is built from: `src/` declares 129 caps under 128 distinct names, of which 111 are tunable, 12 -must stay `constexpr`, and 5 were declared after the sweep was prepared and no measurement has touched -(`kChurnMergeBombMaxFiles`, `kFieldIdCapacity`, `kForPageRowsDefault`, `kForPageUnionSymbolCap`, -`kMaxBlockBytes`) — 111 + 12 + 5 = 128. Neither 112 nor 12 was wrong: they are the FROZEN classification in -`bench/capsweep/tunable.tsv`, and the census beside them is re-read from `src/` on every run, so the two are -different populations and the missing four were five new names minus one (`kSituTestRowsShown`) the sweep -classified and `src/` no longer declares. The paragraph derives all three parts now, states that skew rather -than hiding it, and `capsweep.py emit` REFUSES to render a partition that does not add up — a name classified -in two lists at once, the shape an asserted sum cannot see, exits non-zero instead of publishing. Gate: -`test/capsweepcheck.sh` arm (C) reproduces the document byte-for-byte through that refusal on every run. - -### Fixed — a scoped run said its ranking fell back, having run no ranking - -`--rank-by=churn-decay` discloses a window that mined no commits: the teleport prior is uniform, so the map is -byte-identical to `--rank-by=pagerank`, and saying so is the difference between a degraded answer and a silent -one. Under `--in=DIR` that same sentence was false three ways at once, on a real invocation -(`--rank-by=churn-decay --in=src --since=HEAD`, a window that reads no commit). Nothing is ranked on a scoped -run — the rank vector is default-constructed and zero-filled, which is exactly why the header carries no -`pr_iters=` — so "using uniform (structural) ranking" named a computation that did not happen. "This map" -named a document the run does not contain, since the symbol map IS the counted stub and, with no history -mined, neither `` block rides at all. And the comparison it offered is unrunnable: `--rank-by=pagerank` -is refused beside `--in`, so the reader was pointed at a command the tool rejects. - -The scoped branch now states what did happen — no block rides, the map is the stub, nothing was ranked, so -there is no ranking to have fallen back — and keeps the pagerank equivalence where it is true, on the unscoped -run a reader gets by dropping `--in`. The two sibling callers (the multi-root arm and undecayed -`--rank-by=churn`) pass `stubbed=false` at the call site with the reason recorded: `--in` rides neither. -Gate: `test/recentscopecheck.sh` arm 13e asserts all three claims are gone and that the notice says nothing was -ranked, with 13f the control that the unscoped sentence survives unchanged. - -### Fixed — a pasteable `next=` quoted a tilde no shell expands - -Every `next=` in the tool is built by `nextFlag`, which quoted any value whose first character is `~` -whether or not a flag name preceded it. So a run under `--exclude=~tmp` published `--exclude='~tmp'`, and -because the attribute is XML the escaper rendered it `--exclude='~tmp'` — a replayed argument -corrupted to defend against an expansion that cannot happen. POSIX tilde expansion applies to a word whose -FIRST character is `~`; the word here is the whole argv element and it begins `--exclude=`, and an argument is -not an assignment. Measured on macOS, `sh`/`bash`/`zsh` alike: `sh -c 'p ~root'` passes `/var/root`, -`sh -c 'p --exclude=~root'` passes the literal `--exclude=~root`. The guard is now "word-initial AND no flag -name", a narrowing rather than a deletion — when the value IS the whole word (`src/editplan.h`'s rollback -invocation) the tilde really is word-initial and the quotes are load-bearing. - -The worse half was the gate. `test/nextverbcheck.sh` arm (9) pinned the entity-quoted form and explained it as -a shell expanding `~tmp`, which is wrong about POSIX twice over — the tilde is not word-initial there, and -`~tmp` expands nowhere anyway, since `~user` is expanded only for a user that exists. A gate that pins a false -belief does not merely miss the bug, it defends it against the next person to fix it, so the explanation is -deleted rather than reworded and the measured rule stated in its place. The arm pins the bare form, asserts the -invocation carries no `'` anywhere, and gains a mutation control: a space-bearing value is still quoted, -so the change narrowed the tilde case instead of disabling quoting. - -### Fixed — a churn window says how many commits it skipped as merge bombs - -The churn-decay miner behind `--rank-by=churn-decay` skips any commit touching more than 100 indexed -files — bulk renames, reformats, wide merges — and counted nothing about it, so a `` block -could silently omit the very commit a question was about: a held-out gold commit that touched 71 -source files (more than 100 in all) was invisible to the block, and nothing in the output said a commit -had been dropped. The block now carries `merge_bombs_skipped="N"` on every run, `"0"` included, so its -absence is never ambiguous; the full and compact legends define it and state the 100-file threshold -(`kChurnMergeBombMaxFiles`, now a named constant in `src/gitmine.h`, listed in `docs/LIMITS.md`). On this -repository's own tree the attribute reads `merge_bombs_skipped="5"` — five commits the map had been -quietly built without. Gate: `test/churndecaycheck.sh` arm 7 builds a repository whose HEAD commit adds -101 files and asserts the block reads `"1"`, that none of those files is a row, and that both legends -define the attribute; red on the previous binary (no attribute anywhere), green now. Both legends say -"more than 100 INDEXED files" — the rule counts the files this crawl HOLDS, never the commit's raw file -count, so a commit of 120 `.txt` files and one `.py` is not skipped and the old wording described a -different rule from the code's. The threshold is the named constant at all four call sites now; the two -`--rank-by=churn` walks kept a literal `100` beside a comment claiming parity with it. `merge_bombs_skipped=` -rides the GLOBAL block only: it counts the window's skipped commits, and stamping that number on a -directory-scoped element read as "N commits under DIR were skipped", which is wrong for any DIR smaller than -the repository. STILL UNDISCLOSED, and named here rather than left silent: `--cochange`, `--situ`'s co-change -partners and `--pr-context` apply their own commit-size skip at a cap of 30 with no counter at all — the same -class of silent drop, on three other verbs; disclosing those is a separate round. A window whose -every commit was skipped — a shallow clone of a large tree is exactly this shape: llvm-project at depth -1 is one 183,835-file commit — used to print no block at all, which reads as "no history mined"; it now -prints ``, zero rows and the reason (arm 7h, red -on the previous binary). A tree with no git history still prints no block. -### Fixed — `--expand` chose its serving mode on two different price lists - -`--expand`'s cheapest-complete-answer serving compares the default bundle against the whole file(s) the -requested symbols live in and emits the smaller, disclosing both byte counts on the `` root. The two -candidates were priced by two hand-built counters. The bundle was charged the `` envelope, the root -attributes, the unproven residue, the ranked map and the rendered ``; the file was charged -`wf.rawBytes` plus its own legend — no envelope, no root attributes, no ``, and the file's RAW bytes -rather than the `` blocks that actually carry them. Measured with `wc -c` on a fixture whose one -symbol sits in an 864 B file: the root said `reason="file 1100B < bundle 1193B"` over a document that came -out 1262 B, so the tool selected — and reported — the whole-file form while the bundle it rejected was the -smaller document. This was the SECOND asymmetry found in this one comparison (the first, one review round -earlier, was the whole-file legend), which is the tell that the bug is the two counters rather than the -missing addends. Both candidates now describe themselves as an `ExpandServeDocument` and are priced by one -`priceExpandServeDocument`, which charges the whole served document — envelope, every root attribute, the -mode's legends, the map where it rides, the payload as emitted, the closing tag — and settles the -self-referential `mode=`/`reason=` disclosure with the same ≤4-pass fixpoint `pricedRootAttr` uses for -`est_tokens=`. The reported figures are that one function's return values, so each is now exactly the -document it names. Gates, red first: `expandmodecheck` (4a)/(4b)/(4d) assert that `reason=`'s own byte count -equals `wc -c` of the delivered document in whole-file mode, in bundle mode and in bundle-with-a-ranked-map -mode, and (4c) sweeps seven file paddings across the decision boundary and asserts the served document is -never larger than the candidate it rejected — 2 of those 7 and both identity arms FAIL against the previous -build. `expandtopk0check` (G-b) moves with it: its priced-bundle identity now reads against the document that -mode serves rather than against explicit `--top-k=0`'s undecorated root, which the old price matched only -because price and document omitted the same decoration; it is red on the previous build too (2403 B priced -against 2474 B served). - -Five more from the same review round. The MCP `for` twin appended the `sc=` reading unconditionally while the -CLI lens made it present-only, so a scope-free answer defined an attribute no row carried, and the -signatures-budget exemption hand-built the same decision a second time; both now ask `forIdRouteLegendParts` -once, and `mcpforparitycheck` arm (7) pins the rule in both directions — absent on a scope-free corpus on -BOTH dialects, present on `src` on both (CLI 0 / MCP 1 before the fix). `estchargecheck` arm #18 counted two -of the three droppable legend clauses, so the `route=` reading was pinned in neither direction: with that -clause removed from the binary the arm still reported `clauses=2/2` and PASS, and it is now counted in the -wide run, the probe and the tight control that must have dropped it. `taskroutecheck`'s R-LEG arm ran every -generated command under `eval … || true`, which discards every exit status; it now captures each one and -reports anything that is not 0 (answered) or the documented empty-corpus 1 — measured over the whole corpus, -34 distinct commands, 15 and 19 — and widens its stderr check from the single compact-legend refusal to any -parse refusal. And the three route hooks' mirrored command-word rule asked the shell to split the line, which -never separates a control operator from the word it is attached to: `true; ripwire .` split into `true;` and -`ripwire` and read as not-a-call, as did every `a;ripwire` / `a&&ripwire` / `a|ripwire` / `(ripwire .)` shape. -The rule now lexes the line itself, quote-aware and executing nothing; `routehookcheck` O9 grows to 28 shapes -(19 calls, 9 appearances that run nothing), of which the five operator-attached calls answer 0 on the previous -block, and its byte-identity arm still reports one 147-line text in all three hooks. Last, `docs/LINEAGE.md` -claimed "no network" without qualification in the same sentence that already scopes its dependency clause with -"for the map": `ripwire ` is a documented input form that shallow-clones before it maps (`src/cli.h`, -and `--refetch` forces a fresh clone), so the clause now names that one exception in the sentence's own voice. - -### Changed — agent surfaces ask for the compact legend - -The commands ripwire writes for an agent — the `ripwire wrap` paste block, the skills under `skills/`, -the `` line `--help-task` hands the prompt router, and the runnable line the tool-call router -injects — spelled every XML verb with the default legend, the ~3 KB posture the `--legend` help text -itself tells repeated callers to leave: most of a small `--callers`/`--uses`/`--impact` answer, byte- -identical rows either way. Every one of those commands now carries `--legend=compact` where the verb -accepts it (the XML verbs); `--for` keeps the default legend (its compact legend is its own, and the -first call of a session wants the full one), and the text, JSON and writer verbs the binary refuses -the flag on are untouched. Counted on this commit: 158 `ripwire ` verb commands in 17 skill -files (bodies only — no description changed, so no skill's stop rules or boundaries moved), 9 -commands in the wrap paste block (its 10–20 line band unchanged), 26 `--help-task` routes and the 2 -tool-call routes. Humans running the bare CLI see no difference. Gates, red first: `wrapverbscheck` -arm 7 asserts the flag on every blurb command for the ten XML verbs it spells and its absence on -`--for` (10 FAIL against the previous build); `skilltruthcheck` asserts it on every skill command for -the shipped verb list (152 of 154 missing before the transform) and its absence on `--for`. Each surface also -says how to get the full legend back (owner question, 2026-09-13): the wrap paste block, the router skill's -shared conventions, the three route hooks' injected context and the MCP schema's `legend` field each carry -one sentence — add `--legend=full` when a definition's reasoning is needed (a term you do not recognise, a -floor or cap you need explained, a map a human will read); `wrapverbscheck` arm 8 asserts the sentence on -all four surfaces, red first. - -### Fixed — the route hooks counted a directory named ripwire as a ripwire call - -The `--observe` arm of `hooks/ripwire-claude-route.sh` and `hooks/ripwire-codex-route.sh` closes the -adoption-within-two window by inspecting the next two ripwire calls after a recommendation. It -recognised a call by any token ending in `/ripwire`, so `cd …/ripwire && git log --oneline` — the -directory, in argument position — consumed a window slot, and a real adoption two commands later was -logged as `missed` (found in the local routing analysis of 2026-09-12; the numbers stay local). A call -is now counted only when the command word itself is the binary — `ripwire`, `./build/ripwire`, any -path whose basename is `ripwire` in command position (at the start, after `;` `&` `|` `(` or `$(`, -past leading `VAR=value` assignments) — never a directory argument or a bare word after `echo`. -`test/routehookcheck.sh` O8 and `test/codexpromptroutecheck.sh` carry the regression shape (red on -the previous hooks: rows=3 where 1 was wanted; the Codex twin's position-2 adoption read as missed). - -### Changed — `--for`'s compact legend is present-only, and pinned like every other verb's - -Under `--legend=compact` every other XML verb answers with a legend that defines only the terms its -document carries, measured and pinned per schema in `test/compactlegendcheck.sh`; `--for` did not. Its -native compact dialect was the default dialect's sentences with a schema id in front — 1,177 to 1,216 -bytes on that gate's own fixture — and it was exempt from the per-verb pin by name, so a call an agent -makes more than any other paid the one legend the compact dialect exists to shrink. It now answers with -one present-only comment: a reading for each row and root attribute the bundle actually prints -(`cx`/`ccx`/`in`/`churn`/`amp`/`clone`/`tested`, `sc=` and the `route=` code, `bundle`/`bodies`/`reason`, -the `total=`/`shown=`/`capped=` window, the hops or bodies clause of the serving shape, the tail and the -confidence gauge), and the data notes keep their numbers without their sentences -(`[floor: kept 7 of 40]`, `[doc mentions: 1 doc, 1 symbol; doc_mentions=]`); the three -ceiling-droppable clauses still fall together under a tight `--token-budget` and the dropped note still -names them. Measured with the gate's own splitter (comment bytes not present verbatim in the default -dialect's document) on its fixture probe `--for=geometry`: 915 B on the pre-change binary → **654 B** -here, pinned at **670** as the new `ripwire.for/v1` row (the table's own rule: the largest measured probe, -up to the next 10 B, plus 10), the exemption gone. A1′ first pinned it at 500 from 494 with only its own -clauses present; the merge with #213 put the `coverage=` reading on the same probe, and this round made -the `sc=` and `route=` readings present-only, so the number was re-measured at each step rather than the -clauses trimmed to hold a pin. Per call, default legend → `--legend=compact`, three tasks -measured with `wc -c` on this repository at the lane's merge of `origin/main` 0e3573af: -`pagerank power iteration` 9,875 → 9,338 B (−537, and 25 → 29 signature rows, because the bytes the legend -gives back are spent on rows), `rank graph teleport` 10,121 → 9,812 B (−309, 22 → 25 rows), and the -name-exact `escapeXml` 6,290 → 4,918 B (−1,372). The **delta** is the claim: the absolute bytes carry -`churn=`/`amp=` readings derived from git history, so they move by a few bytes per commit landed. The MCP -`for` twin declares no `legend` field and serves the default dialect only, so its bytes are unchanged. `legendcoveragecheck` holds: -every attribute the compact document carries on its first screen has a `name=` definition in that one -comment, with `next=`, `pure=` and `schema=` on the recorded floor exactly as before. - -### Changed — symbol rows carry a short id (`sc=`) instead of repeating their path - -Every scoped symbol row on the map and on the `--for`/`--pack-task`/`--from-trace`/`--pack-signatures` -signature rows printed its canonical id in full — `id="src/mcpverbs.h::rw::applyCompactToBatchSubs"` on a -row that already sits under `` or carries `p="src/mcpverbs.h"` itself. On this -repository's flagless map that was 137 of 137 scoped rows repeating the path the wrapper had just -printed. The row now carries only the segment nothing else on the page holds, `sc=` (the enclosing scope), -and the legend states the composition: the full id is `p::sc::n`, with `p=` taken from the row or its -enclosing ``. Nothing an agent could address before is unaddressable now — `--expand`, `--callers`, -`--impact`, `--uses` and the MCP twins accept the composed `path::scope::name` exactly as they accepted -the printed `id=`, and `test/scroundtripcheck.sh` proves it: the multiset of ids composed from the new -rows is byte-identical to the multiset the previous binary printed on two fixtures, every composed id -resolves through `--expand` to a body of that path and name, a mutated scope resolves to nothing, and the -old spelling still resolves on input. Two smaller cuts ride the same rows: `route=` is a code -(`name-exact(X)`, `subtoken+body`, `subtoken+body:broad`, `subtoken+body:declined(word;carriers,defs)`) -with its reading in the legend instead of 107 bytes of prose per answer (23 bytes now; the `anchors:` -evidence clause is unchanged), and a `--for` compact bundle merges the same-named callees of one `calls` -block into one `` row (`shown=` still counts callees). Measured with `wc -c` against the -build of `origin/main` (f6a27167) run on this same merged tree, so no corpus drift rides the numbers: the -flagless map of this repository 26,449 → 22,407 B (−15.3%, the same rows), `test/cppqualfix` 2,935 → -2,781 B, `test/nestedqualfix` 2,045 → 1,937 B; a fixture with four scoped rows (`test/accessshapefix`) -grows 9 B, because the `sc=` reading is longer than the `id=canonical(…)` clause it replaces and four rows -do not pay it back. On `--for` the bundle is byte-shaped, so the row savings became ROWS: three tasks on -this tree, same tree and same day, main's binary → this one — `pagerank power iteration` 9,470 B at -`shown="20"` → 9,880 B at `shown="25"`, `rank graph teleport` 10,134 B at 19 rows → 10,022 B at 22 rows, -and the name-exact `escapeXml` 5,977 → 5,846 B. Five more ranked rows for 410 B on the first; three more -for 112 fewer bytes on the second. -The new legend is two clauses, both ceiling-droppable with the confidence clause and both exempt from the -signature-trim charge like every other disclosure: the `sc=` composition rule (`; sc=scope (full id -p::sc::n)`, 29 B) on every answer, and the `route=` code vocabulary (54 B) only on the answers whose root -carries `route=` — the same present-only condition the compact dialect already used, and one shared -spelling for both dialects so a code cannot acquire two readings. The route clause rides the document -rather than living only in `--help`'s `--no-route` entry (where the fuller reading of each code still is) -because a code with no reading anywhere in the answer is an undefined first-screen attribute: -`test/legendcoverage_baseline.txt` is a ratchet that may only be edited downward, and dropping the clause -opened two new lines in it. A 259 B first spelling grew a 2.9 KB fixture bundle by 10% and tripped -`test/forrankordercheck.sh`'s 4% ratchet; the 83 B shipped here crosses it nowhere on the nine frozen -fixture bundles (+0.5…+3.2%). The `--json` -twins mirror the attribute (`"sc"`), so `mcpattrparity` holds without a rename. Pins moved with the -bytes, every one of them RE-MEASURED on the merged tree rather than carried over from either lane's own: -seven compact-legend schemas in `test/compactlegendcheck.sh` (map 810 → 920, map-diff 800 → 910, -pack-signatures 680 → 780, metrics 720 → 820, query 630 → 730, pack-task 820 → 980, pack-top-n -660 → 770) for the one new whole-document `sc=` reading; `ripwire.for/v1` 500 → 690 (measured 678 — A1′ -pinned that dialect at 500 from 494 with only its own clauses present, and the merge brought the -`coverage=` reading onto the same probe); the ten-verb legend loop 4,900 → 4,700 B (measured 4,645 — DOWN, -because A1′'s present-only `--for` legend outweighs what both lanes added); the MCP manifest ceiling -42,200 → 42,900 (measured 42,820); `test/fixture`'s map `est_tokens` 884 → 894; -`test/forrankordercheck.sh`'s q5 9,470 → 9,880, attributed four ways (main tree/main binary 9,464, this -tree/main binary 9,470, so corpus drift is 6 B — the other 410 B is this change, and it is five more -ranked rows); five goldens regenerated for the row shape and the clauses; the printf-parity manifest -re-pinned for `help_all` alone at each of the lane's two merges with `main` (`UPDATE_GOLDEN_EXPECT` -matched both times, 41 labels unchanged); across the whole lane seven of its 42 labels moved — six for the -row-6 row shape and `help_all` for the merged `--help` text. The second merge (`origin/main` 0e3573af) -also brought `help` and `expand`: `help` moved on main alone (#217 re-worded `--replace-symbol-body`'s -summary line) and `expand` on this lane alone (the whole-file serving's `sc=`), so each was resolved to the -side that moved it, while `help_all` — moved by BOTH — was re-measured against the merged build rather than -picked from a parent that never produced that text. - -### Fixed — `--for`'s rung zero fires on the exact ceiling, not on the overshoot allowance - -Under an explicit `--token-budget`, `--for` prices its header against the delivered-byte allowance -(budget × 2.36 × 1.15) and, when the document does not fit, first drops the three explanatory legend -clauses whose loss costs no fact (confidence, tail, the `sc=` rule) before touching anything a reader -would miss. The 1.15 tolerance exists for the residual a lens cannot trim — a first signature is not -divisible — but it also gated that first drop, so a document 1–15% over its budget that still carried -all three clauses shipped `over_ceiling="1"` with them riding: on a fixture whose path left a few dozen -bytes of slack, `test/fornotesbudgetcheck.sh`'s 1640 rung measured est_tokens=1755 and -`test/forrootlegendcheck.sh`'s 800 rung 831 (both CI, PR #215). The free drop is now tried against the -number the root promises (budget × 2.36) and only what remains is judged by the tolerance. On the merged -tree the same 1640 rung reads est_tokens=1515 with eight rows intact, and `forrootlegendcheck`'s arm 2 -(850 since lane for-widen re-anchored it) reads 832 with the root-relative clause surviving. -`test/estchargecheck.sh`'s late-label sweep control is re-anchored to where that residual band now sits -on its corpus (760..1500; hits at 780–810). -### Changed — tests-to-run rows without a runner are grouped by hop distance - -Every tests-to-run row that had no derivable runner said so on the row — `run_unknown="1"` in XML, -`"run_unknown":true` in JSON, `(run: not derivable)` in `--situ`'s text — and on a corpus where almost -no harness has a runner that was the same 16 or 23 bytes repeated once per row: on the RocksDB tree, -`--affected=db/write_batch.cc` listed 127 tests, 126 of them runner-less, and paid 2,016 B of XML and -2,898 B of text for one fact. Rows already come in evidence order (changed, partner, hops ascending, -path), so runner-less rows whose per-row attributes are byte-equal are now served as one row, -`` (JSON: `"p"` — or `"test"` — becomes an array beside -`"n"`; text: `[hops=2] (17): a, b, c (run: not derivable)`), emitted where its first member stood. Rows -with a runner stay single, a group of one stays a `` row, a path that contains a comma is never grouped -at all (`p=` is a comma-separated list and every XML parser undoes an entity before a consumer splits on the -delimiter, so an escaped comma would reappear as a separator and `n=` would disagree with what the reader -counts — the text twin had no escape to undo), and every path is kept verbatim — the multiset of paths -before and after is identical and so is their ORDER, which is what `test/testrowruncheck.sh` arm 12 proves -on a fixture with three hop groups and a runner row -in the middle of one of them, in all three dialects (red on the previous binary). All twelve emitters — -`--affected`, `--exercises`, `--test-gate` XML and JSON, `--situ`, `--pr-context`, `--handoff`, -`--flags --flip`, `--pack-task` XML and JSON, the MCP `situational_awareness` twin and the edit -receipt — render through one seam in `testmap.h`, and the M21(b) rule keeps its meaning: a `` or -`` row carries `run=` or `run_unknown="1"`, never neither. Measured on RocksDB (`wc -c`, same cache, -same commit): `--affected=db/write_batch.cc` 10,668 → 6,992 B, `--test-gate=db/write_batch.cc` 13,242 → -9,747 B (its JSON 11,055 → 7,163 B), `--situ=db/write_batch.cc` 11,769 → 7,357 B, and in the compact -dialect 9,312 → 5,313 B and 11,223 → 7,596 B; 8 `` rows replace 124 single rows (a group covers a -contiguous run only, so the one runner row inside the hops=2 tier splits it in two — order is preserved by -construction, `test/testrowruncheck.sh` arm 12 reads the paths back in emitted order) and the residual -spent on the disclosure is 160 B (XML, 10 `run_unknown="1"`) and 230 B (text) per list. -`--pack-task`'s tests section is byte-budgeted, so it CUTS over its own grouped, escaped rendering: it takes -the largest prefix of the row list whose rendered `` body fits the section budget, found by bisection -(the rendered size is monotone in the prefix length, so the bisection is exact), and counts `shown=`/`total=` -in test files. Measured on RocksDB with `--pack-task="change WriteBatch::Put"` at the default 6,000-token -budget, `wc -c`, same cache, same commit: `` where the pre-E1 bundle named 28, -the whole bundle 11,993 → 12,490 B. On this tree every harness has a runner, so nothing groups and the only -change is the legend that now defines ``: the `--test-gate` legend pin moves 2,720 → 3,000 B (measured -2,957) and the `ripwire.pack-task/v1` compact pin 820 → 880 B (measured 865), both because the compact -dialect and every rows-bearing full legend now define `run_unknown=` and `` — a definition -`--affected` and the compact dialect never carried. That compact `` term now says what the full clause -says, in the full clause's own words: its first form promised "every path verbatim (`,` a comma)", an -escape `testmap.h` does not emit — a path holding `,` is not grouped at all — and it never carried the rule -that a `shown=`/`total=` over these rows counts test FILES, so a reader holding only the compact legend was -told to undo an entity that is not there and disagreed with the full legend about what the pair counts. The -term goes 99 → 194 B and is charged only on a document that carries a `` row (measured on a fixture of six -runner-less tests, `--affected --legend=compact` 501 → 596 B); no pin moves, on this tree or on any gate -fixture, because every harness here has a runner and nothing groups. The two wordings cannot be one constant -— the compact dialect exists to re-spell, not to quote — so `test/compactlegendcheck.sh` arm (R) pins them -against each other, reading the phrases it requires out of `kRunHintLegendClause` itself rather than -restating them, and fails the next release where either wording drops one or promises `,` again (red on -the parent commit's source). The MCP manifest ceiling moves 42,384 → 42,800 B -(measured 42,777) for one 207-byte clause, plus the one-space separator that joins it to the sentence before -it, spliced into each of the two tool descriptions that serve these rows as JSON (2 × 208 B): -`situational_awareness` and `explore` return bare JSON with no legend of any kind, so a caller that -reads `p` as a string has nowhere else to learn that it can be an array. -The clause is rows-gated everywhere it is spliced — `--affected`, `--exercises`, `--pack-task`, the -partitioned bundle, and `--pr-context`, whose legend precedes its files in the STREAM but is now decided -after them: the chosen body is rendered first, the pricer charges the clause per candidate trim level from -that level's own body, and the form the chosen body was priced with is the form written, so the priced -legend and the delivered legend cannot disagree. A corpus-level predicate over-approximated it — a test -file outside the selected range, or a trim level whose `testCap` is 0, bought the clause for a document -with no row — and both now pay nothing (measured on `test/defaultceilingcheck.sh`'s 120-file, no-test -fixture: unconditional, the default bundle went 7,989 → 8,025 tokens over its 8,000 budget; gated, 7,989; -`test/prcontextcheck.sh` pins all four sides, red first). The clause is gated on a COUNT the emitter -reports with its rows, never on a search of the rendered bytes: `--pr-context` charges the trim level's own -row count and the partitioned bundle sums what each slice kept. Asking the bytes was wrong twice — a -`--pr-context` body and a `--pack-task` slice can both carry the literal text of the element inside CDATA, -and `--pack-task="write_report" --partition=2` over a two-file corpus with no test at all bought the outer -clause because one body prints `` (`test/testrowruncheck.sh` arm 15, red first). `--handoff` -and `--flags --flip` spliced the clause unconditionally and now ask the same count; `--handoff` is -byte-budgeted with heuristic rows dropped tail-first, so on a packet with no test row the 180 B it was -paying could evict a real row (arm 14, red first). - -Two byte-accounting rules changed with it. `--pack-task`'s tests section used to group FIRST and cut the -group rows with the generic list cutter under a per-row cap whose estimate was computed on UNESCAPED path -bytes, so a corpus whose test paths hold `&` or `<` rendered wider than the cap admitted; the cutter breaks -at the first over-budget entry, so the whole tail of the section went with it — `run=` singles included. -Measured on a matched pair of ten-test fixtures differing in one byte per name (`&` against `_`) at -`--token-budget=1440`: the control named 5 files and the `&` fixture named none. Cutting over the grouped, -escaped rendering fixes it and is strictly better than cutting the single rows and grouping afterwards, -which would have been safe but spends fewer of its bytes (2 files where grouping-first served 5); across -budgets 1440–1860 the new cut names 6–11 files against the old 5–11, and the `&` fixture never empties -(`test/testrowruncheck.sh` arm 13). And `--pr-context`, which must render a level to price it, rendered -through a helper that returned an empty string on an `open_memstream` failure with no alert at all — a -document could have shipped its legend, root and closing tag around an empty body claiming -`truncated="none"`. Every such render now goes through one seam in `infra/emit.h` (`rw::renderToString`, -which `packtask.h` already had in its own spelling) that reports the failure, and both `--pr-context` exits -fall back to streaming the level straight out: complete, correct bytes, a modelled estimate, and a -`DEGRADED_PATH_ALERT` saying which — serialize.h's own degrade contract. - -Six gates read the PATHS out of these rows, and each had its own reader: since a row can now name several -files, `grep -oE '"tests_to_run":\[[^]]*\]'` stopped at the first `]` (the end of the first group's path -array, so three arms asserted over two and a half rows and passed vacuously), `sed`-based XML readers saw -only the single rows, and the text reader took `$1` of a line that on a group line is `[hops=1]`. They all -want the same thing — the files named, in emitted order — so `test/affectedcheck.sh`, -`test/impactpartitioncheck.sh`, `test/receiptpostcheck.sh`, `test/rootrelemitcheck.sh`, -`test/selectorchaincheck.sh` and `test/testrowruncheck.sh` now all ask `test/testrowpaths.py`, one reader for -all three dialects and both row shapes. Two more gates read these rows and keep their own readers, because -neither asks for the paths: `test/listingpagingcheck.sh` sums `n=` over the group rows to prove the family -never pages, and `test/w3fixlegendcheck.sh` counts path occurrences on a `--situ` line. That shared reader -had two silences of its own, and both now fail loudly with a control in `test/testrowruncheck.sh` arm 16. Its -JSON slicer returned the same nothing for a document with no `tests_to_run` field and for one whose array -never closes, and the path reader turned that into an empty list at exit 0 — so a TRUNCATED document -asserted over zero rows and passed, which is the defect the file was written to end. The two are different -claims: no field is an answer (0 paths, exit 0), an unclosed list is exit 2 with a named reason. And the text -dialect's single-row reader took `(\S+)`, which stops at the first space, so a test path holding one was -reported truncated — a path that does not exist, produced silently. It now cuts the run suffix and the -renderer's own attribute tail (`[changed] [partner] [hops=N]`, in that order and no other) and keeps -everything between verbatim; what the text dialect still cannot resolve is a path holding the literal -three-space `(run: ` opener, because that dialect carries no escaping at all — XML and JSON are exact. - -Three more things the row work left half-said. `rw::renderToString` asked `open_memstream` and then ignored -what `fflush` and `fclose` answered, returning `ok=true` regardless: a memstream grows by `realloc`, so an -allocation failure the per-row writes swallowed surfaces at the flush, and it is the close that publishes the -buffer and its size at all. Reading them anyway is how a SHORT document passes for a whole one — the same -defect as the empty body one size smaller. Both results are now checked, the alert fires, and `--pr-context` -takes the streaming fallback it already documents. The MCP row-shape clause named the key `p`, and only one -of its three producers spells it that way: `situational_awareness` emits `test`, `explore` and the edit -receipt emit `p`. A clause naming the wrong key is worse than no clause, because a caller reads it as a -contract, so it names both per producer while the rules they share are still stated once; the manifest -ceiling moves 42,800 → 43,000 B for a measured 42,973 (the clause 207 → 305 B in each of the same two -descriptions, 2 × 98 B). And `renderToString` called the emitter outside any handler: a throw from it — -`std::bad_alloc` out of the `std::format` fallback is the reachable one, since the point of the seam is to -buffer a document whose size is not known in advance — skipped the `fclose`, the `free`, the alert and the -documented empty-result fallback in one jump, leaking the memstream and its buffer and handing the caller an -exception where its contract says `ok == false`. Measured on this tree with the fault injected: -`--pr-context` aborted at `rc=134` with **zero bytes** on stdout and `libc++abi: terminating due to uncaught -exception of type std::bad_alloc` — the whole document lost, not just its estimate. The seam now catches at -its own boundary, releases what it owns once, discloses, and returns the degraded value its callers already -read, so the same run exits 0 with a complete 14,627-byte well-formed document carrying the same 20 `` -rows as the undegraded control. The alert names the throw rather than borrowing the buffer's message, which -on that path would be a wrong cause attached to a right consequence. Because a throw path is otherwise -unreachable from a gate, it is driven by an in-source fault switch in `serialize.h`'s -`isChargeBufferFaultInjected` shape — non-NDEBUG only, read once per process, exact `"1"` the only ON value — -and `test/prcontextcheck.sh` arm (F) asserts the whole contract with its own observability probe, red on the -parent commit (`rc=134`, 0 B, no alert). That switch carries the `INFRA_` prefix rather than this project's: -everything under `src/infra/` is built to travel to another repository, and `test/infraportcheck.sh` (C) -refuses a layer file that names the host — it caught the switch's first spelling, which is the gate doing -exactly what it exists for. - -### Fixed — an unmeasured `est_tokens` said nothing, a no-throw contract threw, and two test-row readers still went quiet - -Six defects from one review, each of them a surface that was silently wrong rather than loudly broken. -**`--pr-context` shipped a wrong `est_tokens` with no disclosure.** When a trim level's measurement render -fails, `prRenderLevel` returns an EMPTY body; the ladder priced that empty body, the price fit, and the root -printed it — while `writePrContext` correctly streamed the complete untrimmed floor. The only signal was -`DEGRADED_PATH_ALERT`, which `src/infra/Diagnostics.h` compiles to `do {} while (0)` under `NDEBUG`, so the -binary a user installs printed a modelled number with nothing at all saying so (non-negotiable #3). The bytes -were never the bug and are unchanged — a failed measurement may not decide what the answer contains — so the -fact goes where this class of fact already lives: `truncated=` now carries `;est-unmeasured`, re-priced with -the label in place, and the legend defines it in the same voice as `budget-floor-exceeded`. That label is 15 -bytes and can ride beside `budget-floor-exceeded`, which takes `prBudgetTail`'s worst case from 248 B to -263 B: `tail[256]` (SEVEN bytes of margin, as `test/fixedbufsweep.sh` had warned in terms) becomes -`tail[320]`, 56 B of margin, and the sweep's row moves with the measured recomputation. `rw::formatTo` was -not what had been saving it — it truncates silently and its return is not read there, so an overrun would -have dropped the closing quote of `truncated="` and shipped a malformed root with no diagnostic. -**`renderToString`'s no-throw contract had a throwing last statement**: `out.text.assign( buf, sz )` is the -one allocation on the success path and sat outside the handler, so a `std::bad_alloc` from it escaped a -function documented to return `ok == false`, and jumped the `std::free( buf )` two lines below on the way -out — leaking the memstream buffer. It is caught in its own handler (the two failures need different -cleanup: the emitter's throw owns an open stream, this one owns only the buffer) with its own alert literal, -and control falls through to the single `free()`, so the buffer is released exactly once on every path. -Proved by `INFRA_FAULT_RENDER_COPY_THROW`, the twin of the emitter switch, in `test/prcontextcheck.sh` arm -(G) — red on the parent commit, and honest in both flavours: the switch and the alert live only on the -non-`NDEBUG` build, so the plain-flavour leg proves the degrade and the `NDEBUG` leg asserts only that the -verb is intact and that no false disclosure appears. The `est-unmeasured` LEGEND definition is asserted on -every flavour, which is the point of moving the disclosure off the alert. **The shared test-row reader's -malformed-field detector had a hole of its own species**: `test/testrowpaths.py` found `"tests_to_run"` and -then scanned arbitrarily far forward for a `[`, so `{"tests_to_run":null,"other":[{"p":"ghost.cpp"}]}` -sliced the NEXT field's array and returned `ghost.cpp` at exit 0 — a foreign field's paths served as this -field's answer, where the docstring already promised a `TestRowParseError`. The value is now read -adjacently (past the key, a `:`, optional whitespace, then `[` or raise); `null`, a number, a string and an -object all take the raise, in both `paths` and `jsonlist`, with a well-formed array and JSON whitespace as -controls (`test/testrowruncheck.sh` arm 17). **And two path readers had never been converted.** A census of -`test/` over the four shapes the reader was written to replace found `test/affectedcheck.sh`'s `tset()` — -in the file the reader's own docstring names among those it converted, so that claim was false — splitting -EVERY row's `p=` on `,` including a single row's, which turns a comma-bearing path (never grouped, by -`testmap.h`'s refusal) into two names that name nothing; and `test/testgatecheck.sh`'s `tset()` matching -` --help-task=""` now answers -those with the `recency-window` intent under `ripwire-fresh-eyes`. The route is conjunctive in three -parts, because two are not enough: a TIME word, a MOTION word, and a word naming the corpus (or a -directory of it the task named) — a time word alone is usually part of a compound noun, and a time word -plus a motion word is also a sentence about a supplier's terms last quarter. An explanatory question is -never this route however many of the three it holds, and the working tree stays `--situ`'s question. A -directory is composed into `--in=DIR` only when the corpus really holds it AND the running build ships the -flag, read off the flag table itself: a router that recommends a flag its own parser has no row for hands -back a command that exits non-zero on the first paste. Cues are matched WORD-BOUNDED, which is not a -detail: with the substring spelling `here` occurred inside where/there, `source` inside outsource, `file` -inside profile and `code` inside codec, and a sentence about a supplier revising their terms recommended -the churn window at `confidence="high"`. That holds for the multi-word cues too, which delimit their own -interior and nothing at their two ends — `show documentation` contains `how do` and `show issues` contains -`how is`, so both of those questions about this repository's history tripped the explanatory guard and lost -the route that answers them. The route also sits BELOW the weighted tier, which is how it -reads a dirty worktree: on a dirty tree `is my diff safe to merge, i changed these files recently` is -still the `review-diff` question, and that route wins before this one is consulted. Held out -(`bench/taskroute_eval.py`, the committed 225-row corpus plus 24 rows for this round, split by its -content-hash rule): accuracy 0.939 → **0.946** test, 0.946 → **0.950** dev, precision 1.000 and harmful -0.000 unchanged. The 225 pre-existing rows score the same three numbers on the new binary — and, measured, -**0 of them reach the new route at all**, so that identity is reported as the near-vacuous check it is -rather than as evidence. - -**And the first call now names the widening step.** `--for`'s file-grain page was named only by a thin -ANSWER's own `next=` — one call too late for an agent choosing what to run first — so every `--for`-shaped -recommendation carries the page as its own `next=`, keyed off the INTENT (a task that merely quotes the -flag inside another verb's argument gets none) and spelled by `forpage.h`'s own `forWidenNext`, so it -obeys the same quoting and the same 120-byte ceiling every other `next=` obeys. The `--help-task` document -also gains the LEGEND it never had in the default dialect: every attribute on its only screen was -undefined, and it now joins `legendcoveragecheck`'s enumeration and `nextverbcheck`'s population. - -The shapes this release adds are also named where an agent actually reads them: the recency window with -`--in=DIR` and `merge_bombs_skipped=` (ripwire-fresh-eyes), the thin-answer `coverage=` gauge and the -`--for … --limit=40` page (ripwire-orient and its `map-before-you-read` companion), the `p::sc::n` -composition of a row's identity, and the grouped `` tests-to-run row -(ripwire-change-check). **A new gate keeps it that way**: `test/agentsurfacecheck.sh` is a ratchet over all -163 long flags `--help` advertises — each is named in a skill body or the `ripwire wrap` primer or recorded -on a committed floor with the reason it is still a gap (5 lines today) — plus a per-shape arm that requires -the term and its verb within five lines of one another on one surface, probing the binary first so a -surface never promises what the build cannot parse. `docs/COMMANDS.md` is deliberately not an accepted -surface: it names every flag by construction, and a gate a generated document satisfies for free cannot -fail. - -### Fixed — the reference guide said things the binary does not - -@heliocipher's reference guide was verified claim by claim against a 0.6.0 build, and its flags held up: of the 82 -`--` tokens it named, the 72 that are ripwire flags all exist and are spelled as it spells them (the other ten -belong to `cmake`, `xmllint`, `graphify`, `skills/install.sh`, or are anchor fragments). What it got wrong it -mostly inherited from this repository. -**"Dynamic dispatch contributes no edge"** was the opposite of the truth — a virtual call emits one edge per -candidate in the receiver's inheritance cone, each `prov="split"` and counted in `amb=`; a four-class fixture -returns three edges, not zero, with CHA-lite correctly dropping the same-name method of the unrelated class. The -honesty section was understating the tool, which costs a reader's trust the way overstating it does. -**`MSVC 19.36+`** was offered as a supported compiler beside an operating-system row naming only macOS and Linux; -it is not a target, and the row now points Windows users at WSL2. **"18 task-shaped skill files"** was one of three -defensible counts of one directory — 17 routable (`skills/*/SKILL.md`), 18 `SKILL.md` files in all -(`skills/hermes/` holds a Hermes-native one), 16 activated for every agent (`ripwire-opt-remarks` carries -`audience: contributor`) — and the README stated two of them, neither labelled. **"208 compile-time caps"** is 210 -by `docs/limits_build.py`'s own derivation from `src/`. **"Directory symlinks are not followed"** understated the -limit: no symlink is followed, and a symlinked source file is not indexed at all, so a tree that reaches its -sources through links reads as if they were not there. **Exit code 2 was missing** from the exit-code table, which -is the one a CI script most needs — a policy gate fired (`--arch`, `--scan-skill`, `--quality-delta`), not an -unknown failure. And one sentence sent a reader to `--scan-skills` to check a single file, which is the directory -verb; the file verb is `--scan-skill=FILE`. - -Unstated, and now stated: `git` is a runtime dependency for the history-backed verbs, which refuse with exit 1 and -a named reason rather than answering thin — `--map-diff` and `--rank-by=churn` answer and disclose the uniform -fallback, `--dmm` and `--pr-context` return an explicit unavailable row. A git URL as the root is the one thing -that touches the network. The write verbs return a receipt — region, `blob_sha`, contract check, tests to run — -so an agent never re-reads the file, and their payload is a whole definition, signature included, not a braced -body. Test coverage is read from call edges out of indexed test symbols, so a shell suite that runs a built binary -as a subprocess is invisible to it (`harness=script`, `reaches=0`) — which is this repository's own shape. `--json` -is an allow-list of nine verbs. `--quality-panel` has a `strict` preset that drops the two families which reshuffle -on unchanged code. Several verbs stay single-root in a multi-root run. Section 3.2 now splits by why a reader is -there — `scripts/pgobuild.sh` for a binary to use, the plain tree for work on the tool — and scopes the -`NDEBUG`/`DEGRADED_PATH_ALERT` warning to the development tree, where it belongs. - -`--replace-symbol-body`'s one-line `--help` summary said it replaces a definition's *body*. It replaces the whole -definition, signature included, as its own long help already said and as both insert verbs say. An agent that -believed the summary sent a braced body and deleted the signature — disclosed in the receipt as -`post_check_unavailable`, not refused. - -`test/readmedriftcheck.sh` gains three arms, so these counts cannot drift again: **(J)** the skills count, pinned to -the routable set and to the install fold's "sixteen of the seventeen"; **(K)** the `--json` allow-list, harvested -from `--help=--json` and required to match the guide in both directions, so a verb that gains `--json` support fails -the gate until the guide is updated; **(L)** the cap inventory, pinned to `docs/limits_build.py`'s derivation. Each -carries its own mutation control. Arm **(B)** took `head -1`, and so pinned one of the *two* sites stating the flag -count — the reference guide's copy had been free to drift since it was written; it now checks every site and names -the line that disagrees. `CONTRIBUTING.md` gains the rule those arms encode — an advertised count is an enumeration, -and if a set can be counted more than one way the prose must say which set — and the stale-object build hazard, -which until now lived only in `CLAUDE.md` ([#217](https://github.com/redhat-et/ripwire/pull/217)). +## [0.6.1] — 2026-09-14 -### Added — --for pages its answer one file per row, and says when to widen - -On the pre-registered follow-up ladder (a 2,066-file C++ corpus pinned at one commit, the frozen 30 -questions, six deterministic steps per tool, no model in the loop), every ripwire follow-up completed 0 -answers through step 4: `--for`'s `next=` pointed at `--expand` (a body, not a wider list), `--top-k` was -inert on `--for`, and `--format=candidates` is symbol-grain (40 symbols is about 18 files in 11 KB). The -one follow-up that completed answers in that ladder was a file-grain page — one row per file, about 6 KB. -Local telemetry had `--for` → `--expand` followed 0 of 259 times. - -`--for=TASK --limit=N` (`--offset=M` pages it) is now that page: a `` document of one -`` row per positive-score file, `p=` spelled root-relative exactly as every other -verb spells it, ranked file-first by `score=` — the IDF-weighted share of the query's subtokens the file's -top 8 symbols cover between them (a term counts once however often it recurs, so one huge file cannot -monopolise; ties by the best symbol's lens score, then path). The root carries the house paging vocabulary -(`shown= total= capped= has_more= next_offset= offset= limit=`) and a `next=` naming the next page. -When the answer is THIN — the top-ranked symbol's name, doc or body carries under 50% of the query's -IDF-weighted subtokens (an unmatched subtoken weighs as the rarest, so a `(#12147)` token lowers the share -honestly), or the ranked head spreads over fewer than 3 files — `--for`'s root carries `coverage=` (that -share, whole percent) with its legend clause, and the r=1 row's `next=` names `--for=TASK --limit=40` -instead of the body. A confident answer carries none of the three and is byte-identical to before; the -`--json` and MCP twins follow the same present-only rule. The MCP `for` twin takes the same -`limit`/`offset` and serves the same page through the same renderer. Beside the page every bundle-shaping flag is refused, never ignored (`--limit=0` and non-numeric -values were already refused). `--top-k` stays inert on `--for` and `--help` now says which flag widens. - -Measured, on the ladder re-registered with the page as step 2 on the `--for` shapes: ripwire's -complete@step row is unchanged at 14/14/14/14/17/17 — the page completed no question, because the seven -misses it ran on hold 3–21 gold files each — while adding gold files on four of the seven (+2, +1, +3 and -+6 files) at 5,539–6,212 B per page (mean 5,841 B), and the thin rule named the page on 4 of those 7 -misses. The frozen-30 single-call instrument is unchanged at 14/30 complete and 42/129 gold files named; -its median bytes-to-answer is 6,348 B (5,988 B before: 10 of the 12 `--for` questions on that instrument -are thin — commit subjects with a `(#NNNN)` token, "how does A reach B" questions — and carry the clause; -the 2 confident ones read the base again, and the 18 non-`--for` questions moved by the 2–4 B the git -stamp moved on every verb). Gate: `test/forwidencheck.sh` — a generated 33-file fixture whose gold file sits at page rank 13 -and is absent from the default head and tail; one row per file, determinism, paging with no overlap, -`coverage=` defined in both dialects, thin versus confident `next=`, the refusals, MCP parity — red on the -pre-change binary. The byte pins that ride a thin `--for` header -(forrankordercheck's fixture rows, forrootlegendcheck, compactlegendcheck's loop, the two `--no-route` -goldens) were re-anchored with the measured number; the confident ones read the base again. - -### Fixed — the review round: a ceiling priced in the wrong unit, and a shape that outlived its output - -Twelve findings from the 2026-09-13 review of this lane, each reproduced before it was touched. - -The one that changed behaviour for every budgeted call: `--for` and `--pack-task` tested their ceiling -rungs at `kMinBytesPerToken` (2.36) while `est_tokens=` and `over_ceiling=` price the delivered document -at `kBytesPerTokenDefault` (2.50). A lens therefore spent rungs against a ceiling it was not measured -against, and dropped legend clauses from documents its own root reports as conformant: -`test/cppqualfix --for="widget ping make box" --token-budget=1200` printed `est_tokens="778"` with no -`over_ceiling=` and had dropped all three droppable clauses "(ceiling)". `rw::ceilingBytes( budgetTokens )` -is the one expression for what a root promises, and the ladder now takes two ceilings: the as-built and -task-echo rungs (the echo is a byte-for-byte duplicate of `task=`, so spending it costs a reader nothing) -aim at the exact ceiling, while dropping `route=` and labelling the bundle keep the 1.15 first-entry -tolerance, which exists for a residual a lens cannot trim. The same query now reads `est_tokens="1146"` -at `--token-budget=1200` with every clause riding. No tolerance was widened and no ceiling was raised. - -**And then the rung stopped being a byte test at all.** Getting the RATE right left the deeper half: a byte -comparison cannot express this root's promise, because `est_tokens` is not bytes ÷ 2.50. It prices markup at -`kBytesPerTokenDefault` and the `--detail` / auto bodies at `kBytesPerTokenBody` (3.80) — one rate per kind — -so comparing the raw document total against `budget × 2.50` charged every body byte 1.52× what the root charges -it. The same test also assembled its candidate from RESERVES and from the auto section whether or not that -section was rendered, which is not the document stdout receives. Both errors point one way: a document its own -root reports as conformant was judged not to fit, and three definitions a budgeted reader has no other source -for were spent to buy headroom that was already there. Measured on a git-less five-symbol fixture at -`--detail=1`: at every budget in **1069..1099** the kept document prices at `est_tokens="1069"` with no -`over_ceiling=`, and the rung dropped all three clauses anyway and delivered `est_tokens="715"` — 354 tokens of -headroom spent to buy nothing. At 1090..1099 even the raw byte total fitted (2,736 B of 2,737) and only the -reserve-priced half vetoed. The exact ceiling is now the token comparison itself, asked once on the finished -document, so there is no second expression to disagree with the first; the allowance rungs stay byte-based, -which is their contract. Gate: `test/estchargecheck.sh` #18, which reads the price off a wide run and probes -five tokens above it rather than pinning a budget — red on the pre-change binary at the probe arm, green at -its control (the rung must still fire where the drop is real). - -**One correction to the record.** The residual this entry previously reported — `ripwire . --for="pagerank -power iteration" --token-budget=2500` delivering 5,769 B against a 6,250 B ceiling with its clauses dropped — -was not an instance of the defect above, and still behaves that way. The 481 B of headroom is the document -AFTER the drop; the document that kept its clauses prices at `est_tokens="2684"` (measured at -`--token-budget=2700`, the first budget at which the same query keeps everything), which does not fit 2,500. -The rung was right there, and the leftover headroom is the granularity of an indivisible ~900 B clause trio. -The real defect needed a body-rate document to show itself, which that bundle (`bodies="0"`) is not. - -Rung zero also stopped being byte-negative. It removed 110–164 bytes of clauses and spliced a 161-byte -note naming them: on a route-less compact answer that is **+51 bytes**, a rung that made the document it -was shrinking bigger and cost the reader three definitions to do it. The candidate is built and compared, -and a drop that does not pay is not taken. - -The dropped-clause note itself described a different document. Four constants picked by one `coverage=` -lookup: the thin spellings named neither `sc=` nor `route=` though rung zero clears that reading on a thin -answer too, the default spelling claimed `route=` had been dropped under `--no-route` where it never rode, -and the compact spellings named `sc=`, which that dialect defines in a clause rung zero does not touch. -One assembler builds the note from the four facts that decide what was there to lose. - -Both identity readings are **present-only** now: `sc=` rides when a served row carries a scope, `route=` -when the root carries the attribute — one decision (`rw::forIdRouteLegendParts`) shared by the append, the -signature-charge exemption, the CLI lens and the MCP twin, which had been mirroring it by hand at four -sites. A corpus of free functions pays nothing for the vocabulary of scope: `test/anchorfix`'s and -`test/routefix`'s goldens are byte-identical to their pre-lane selves again. - -Three surfaces were answering in shapes the tool no longer produces. The MCP **file page** composed -`"routed: " + reason` by hand, so one server answered its bundle `route="name-exact(pick)"` and its page -`route="routed: name-exact(pick)"` — a spelling no legend defines; one producer (`routeNoteOf`) serves all -four sites. `--expand`'s **whole-file** serving still printed `id="PATH::SCOPE::NAME"` inside a `` that had just printed the path, on a document carrying no legend at all; it prints `sc=` and the -root states the composition, and the compact dialect gained the `ripwire.expand-file/v1` schema, because -`--expand`'s two servings share no element and one purpose line had been describing the wrong one. -`docs/COMMANDS.md` was rebuilt from a capture re-recorded on this binary in a **ref-clean clone** (96 `id=` -rows → 26, all of them JSON-RPC and cluster ids; 19 `id=canonical(…)` legends → 1, the `--uses` row's -`in_id=`, which names a different symbol and is correct). `bench/shotgun/cc_static.py` keyed on `id`, which -is absent now, so it fell through to `path::name` and collapsed two same-named methods of one file into one -key — a silent miscount in a benchmark; it composes `p::sc::n`. - -A cap could cut an answer it did not need to: a merged callee row was charged `name+16` while printing about -four bytes, so a block of overloads exhausted its budget early and wrote `capped="1"` over a listing that -would have fit. Charged at what it prints. And `l=` was appended in rank order, so one fact had two -spellings between queries (`l="70,69"` / `l="69,70"`); sorted ascending. - -Three gates were enforcing or reporting the wrong thing. `attrvocabcheck` arm 8 matched map rows by the -retired `id=` spelling, checked zero rows and printed a PASS; it is re-keyed and now fails on zero checks -(six rows cross-checked). `skilltruthcheck` held a hand-typed list of sixty verb names that included `zoom`, -so it enforced `--zoom --legend=compact --mermaid` — a command the binary refuses; the arm now RUNS each of -the 43 distinct `--legend=compact` commands the skills spell against an empty directory and reads the -refusal, which immediately found two more broken lines in `ripwire-quality-bar`. `taskroutecheck` gained the -same probe over all 34 commands the router generates, and the router applies the posture once -(`rw::legendCompactAppliesTo`) instead of in 26 hand-edited strings. `legendcoveragecheck`'s shared-name -floor shrank by the four lines it had been reporting as no-longer-reproducing. - -The route hooks and their own meter disagreed about the same command line, and the substitution rate is a -ratio of those counts. The observe regex missed every wrapped invocation an agent types (`time -./build/ripwire`, `sudo`, `env X=1`, `xargs`, `exec`, `nohup`, `if ripwire`, `{ ripwire`) and still matched -`git commit -m "fix; ripwire hook"`. `rw_is_ripwire_call` is the shell's own model — walk the words, ask -whether any command-position word basenames to `ripwire` — mirrored byte-identical in the three hooks and -asked by the meter too; `routehookcheck` O9 diffs the copies and reads 18 shapes. - -Pins moved, every number re-measured on this build: `test/compactlegendcheck.sh`'s table was re-derived from -its own stated rule (largest measured probe, up to the next 10 bytes, plus 10), which seven rows had not been -following — map 892 → pin 910, map-diff 885 → 900, pack-signatures 759 → 770, metrics 798 → 810, query -707 → 720, around 760 → 770, pack-task 974 → 990, pack-top-n 745 → 760, `ripwire.for/v1` 654 → 670, and the -new `ripwire.expand-file/v1` 230 → 240; the ten-verb loop reads 4,645 B under its 4,700 pin. - -The last two came in without review threads, and both were documents contradicting something the same -repository already states. **The compact-legend policy was not being audited in the direction that matters.** -Every verb command an agent-facing skill spells carries `--legend=compact` where the binary accepts -it (`--for` exempt, its compact legend is its own), and `test/skilltruthcheck.sh` had two arms for it — but -both start from a command that ALREADY carries the flag, so they can only catch a flag that does not belong. -A command that should carry it and does not was invisible to the whole gate, which is how -`ripwire --rank-by=churn-decay` shipped flagless in `ripwire-fresh-eyes`'s pass 1a. Seven spans across -six skills are fixed (pass 1a and the `--help-task` route beside it, `--max-tokens=3000`, `--no-ignore`, -`--pattern=`, `--run-trace=`, and the two `--grep-in=any` recipes in `ripwire-security-scan`), and the gate -grew the missing direction: a flagless span must carry the flag when the bare command emits XML on an empty -corpus AND appending `--legend=compact` is not refused — both halves asked of the binary, never of a list. -The XML precondition is what makes it sound rather than noisy: 21 spans look like violations without it and -6 were real, because a placeholder operand (`--arch=rules.txt`, `--scip=index.scip`) fails before the legend -check is ever reached and its silence would otherwise read as consent. *Floor, stated rather than implied:* -the arm skips every span whose operand cannot resolve on an empty corpus, so it is a floor on the policy and -not a total — those spans are unproven in both directions, not proven exempt. A positive control (a flagless -`--flags` must classify as a violation) keeps a green from being the classifier failing silently; red on the -pre-fix tree named 5 of 33 flagless spans. - -**And the README contradicted its own dependency table.** The guide's opening said "It has no runtime -dependencies" while the table ~120 lines below says "None for the map itself. The history-backed commands -need `git` on the path, and a repository to read" — and names the twelve commands that do. The prose now -scopes the claim the way the table does and names `git`; the no-API-key, no-embeddings, no-index-server and -no-daemon claims are unchanged, because those are true unconditionally. `docs/LINEAGE.md`'s comparison bullet -carried the same unscoped sentence and is scoped identically. +**A header selector answers only with the definitions it can tie to that header, every number a compact answer prints +comes with its definition, and the answers an agent reads got smaller.** Outside contributors wrote the Elixir +module-and-arity resolution (**@henry-hz**), taught `--scip` to read the indexes scip-java writes (**@dpunosevac**), +made the callers answer's `next=` pointer land on the call site it promises (**@antoleod**, in their first +contribution to ripwire), wrote the README's reference guide (**@heliocipher**), and taught the Ruby dependency view +that a constant argument and a rescue class are dependencies (**@andriytyurnikov**). Each is named below, beside the +entry their work produced. -### Fixed — a header's declaration no longer widens to an internal-linkage definition (parser version 96) +### Highlights -The decl-to-def widening behind every `file:name` selector (`--callers=api.h:helper`, `--impact`, -`--uses`, `--safe-delete`, the MCP twins) kept a same-named definition when its file `#include`d the -declaring header. That proof is per FILE, and a translation unit that includes `api.h` for its own -reasons may define an unrelated `helper` in an anonymous namespace or as a namespace-scope `static` — -an overload (`helper(double)` beside the declared `helper(int)`) compiles, and by name it was gathered -and served. Internal linkage makes a definition visible to its own translation unit alone, so no other -file's declaration can stand for it. Raised by CodeRabbit on #139 after merge, outside the diff. +**The release ran against ripwire's own instruments, and the instruments say what moved.** + +*What the instruments are.* Three readouts, all registered before the work began, and none of them a model's opinion. +A frozen bank of 30 retrieval questions, each answered in ONE call on a 2,066-file C++ corpus pinned at one commit, +scored on the gold files the question's answer must name. A follow-up ladder over the same bank: six deterministic +steps per tool, no model in the loop, scored on how many questions reach a complete answer by step N. And a held-out +draw registered separately, so a round cannot be tuned onto the bank it is graded on. Every figure below is the same +question asked of two binaries on the same corpus at the same commit, `wc -c` on stdout, warm cache. + +*What got better.* The work this round was routing and shape, not ranking: giving a question a scope it could not +state before (`--in=DIR`), a widening page when the single-call answer is thin (`--for … --limit=N`), and one row per +group where the answer had been repeating one fact per row. Complete answers and bytes-to-a-complete-answer are the +two numbers that decide whether that paid; the re-measure on those instruments is not part of this release's record, so +what this section stands on is the per-verb measurement in each entry below, every one naming its corpus and method. + +*Where the bytes went.* Three shapes account for nearly all of it: a "what changed recently in this directory" +question that used to be answered with a whole-repository map now collapses that map to a 61-byte stub and adds a +scoped window; every scoped symbol row on a map drops the canonical id it had just printed the path half of, keeping +`sc=` instead; and a tests-to-run list on a corpus whose harnesses have no derivable runner states that fact once per +group instead of once per row. None of the three drops a row, a path or a disclosure — the multiset of answers is +unchanged in each case, and each entry below names its corpus and its method. + + +**Elixir resolves modules and arities statically.** Calls resolve to the module, name and arity they name, instead of +by name alone: lexical aliases, filtered imports, default arguments, pipes, captures and delegates. Nested modules and +each target of a multi-target `defimpl` have separate identities, types and callbacks are navigable, and CLI and MCP +use-site queries share one set of rules. Macro expansion, `__using__` and calls inside `unquote(…)` / `bind_quoted:` +remain static-analysis limits and are documented as such (@henry-hz, +[#81](https://github.com/redhat-et/ripwire/issues/81), landed as +[#207](https://github.com/redhat-et/ripwire/pull/207)). + +**`--scip` works with scip-java.** SCIP writers encode an occurrence's range in one of two ways, and ripwire read only +the deprecated one, so every index scip-java writes was silently ignored and `--scip` changed nothing. It now reads the +typed form first, as `scip.proto` asks. On spring-petclinic the precise overlay went from no matches to 79% of +occurrences (@dpunosevac, [#198](https://github.com/redhat-et/ripwire/pull/198)). + +**Numbers that shipped without a definition now have one.** `graph_unindexed=` shipped in 0.6.0 with no definition on +`--lego`, `--verify` and `--nonlocal-state`, and under `--legend=compact` on every XML verb except `--connect` +([#169](https://github.com/redhat-et/ripwire/pull/169)). Compact answers also carried `declined_calls=`, +`unproven_defs=`, `pr_iters=`, the map header's own counts, `--impact`'s blast-radius counts, `--safe-delete`'s verdict +fields and the `--communities`/`--community` structure counts with no definition; each is defined now, and the compact +pins follow the definitions rather than the definitions being trimmed to fit one pin +([#185](https://github.com/redhat-et/ripwire/pull/185), [#189](https://github.com/redhat-et/ripwire/pull/189), +[#203](https://github.com/redhat-et/ripwire/pull/203)). A budgeted `--for` that drops legend clauses to fit its +allowance now names the attributes whose definitions it dropped ([#174](https://github.com/redhat-et/ripwire/pull/174)). + +**A header selector answers only with what it can prove, and says what it dropped.** A `file:name` selector that names +a C++ header declaration is widened to the definitions the declaration stands for. The widening matched on the name and +the enclosing scope, and that scope drops namespaces, so `--callers=a/Store.h:putObject` counted `callB` in +`b/Store.cpp`, a caller of a different `Store`, and a free function matched on its name alone. A definition is now kept +only when its file is the header or includes it, resolved path-precisely +([#173](https://github.com/redhat-et/ripwire/pull/173)). What the proof drops is counted as `unproven_defs=` on +`--callers`, `--callees`, `--impact`, `--safe-delete`, `--path`, `--uses`, `--mentions`, `--verify` and `--affected` — +all but the first two had answered from the declaration alone and printed a clean zero +([#190](https://github.com/redhat-et/ripwire/pull/190), [#195](https://github.com/redhat-et/ripwire/pull/195)) — and on +every verb that resolves a focus symbol at all, `--edit-check` included, where an `incompatible="0"` beside +`unproven_defs=` is now stated to be an incomplete read rather than a safe edit +([#210](https://github.com/redhat-et/ripwire/pull/210)). On ripwire's own tree, over every `file:name` selector whose +selection is all declarations, the binary after #173 shrank 89 of 4,322 answers compared with the one before it, and +grew none. + +**The declined-call index fits in memory on a tree the size of llvm.** The call graph keeps, for every call the +resolver declines to bind, the list of candidates it declined between. Those lists were stored once per call, so the +structure grew with calls × candidates: 27.9 M entries, 114 MB, on llvm-project. One stored copy per distinct list +makes that 9,879 distinct lists and 62,359 entries — **368 KB** — with every count and every byte of output unchanged +([#208](https://github.com/redhat-et/ripwire/pull/208)). + +**The commands ripwire writes for an agent ask for the compact legend, and say how to get the full one back.** Every +`ripwire ` command in the skills, in the `ripwire wrap` paste block, in the prompt routers and in the tool routes +now carries `--legend=compact` where the verb accepts it — 158 skill commands, 9 wrap commands, 26 `--help-task` +routes and 2 tool-call routes. One sentence per surface, and not one more, says to add `--legend=full` when a +definition's reasoning is needed. The bare CLI is unchanged: it still answers with the full legend +([#215](https://github.com/redhat-et/ripwire/pull/215)). Alongside them, `--for` now pages its answer one file per row +and says when the single-call answer is thin enough to widen +([#213](https://github.com/redhat-et/ripwire/pull/213)), and `--rank-by=churn-decay --in=DIR` answers "what changed +recently in this directory" without a whole-repository map +([#212](https://github.com/redhat-et/ripwire/pull/212)). + +**An agent can ask for the recency answer in words, and every new shape of this release is named where an agent +reads.** The task router had no churn or recency intent at all, so `what changed recently in db` and `who touched this +lately` abstained at `score="0"` and the churn window was unreachable from a task said in words. It routes now, and +composes `--in=DIR` only when the task names a directory of the corpus and the running build's own flag table ships +the flag. Every `--for`-shaped recommendation carries the widening page on the FIRST call rather than only after a +thin answer, four skills name the shapes this round adds, and a new ratchet keeps it that way: a flag `--help` +advertises with no agent surface, or a surface promising a shape the build refuses, goes red +([#218](https://github.com/redhat-et/ripwire/pull/218)). -Every C and C++ definition now carries a syntactic `internalLinkage` bit — inside an anonymous -`namespace { }` at any depth, or carrying a namespace-scope `static` (a class-scope `static` member has -external linkage and is not marked). The widening keeps such a definition only for a declaration in its -own file and otherwise counts it in `unproven_defs=`, so the reader still learns that same-named -definitions exist which no row covers; the bare-name selector still shows them, as the legend says. +### Upgrade notes -Measured on the gate fixture (`test/decltodefcheck.sh` arm B2: a header, its defining `.cpp`, one real -caller, and two including TUs with an anonymous-namespace and a `static` overload): `api.h:helper` -answered `count="3"` on main where `api.cpp:helper` answered `count="1"`; it now answers `count="1"` -naming the one real caller, with `unproven_defs="2"`. On this repository at `1cf3086e` the default map -is byte-identical and none of the 11 header-qualified `--callers` selectors over `src/ingest.h`'s -declarations moved (the tree has no colliding internal-linkage overload). `kParserVer` 95 → 96 and -`kCacheVersion` 21 → 22 (the def record gains one byte) with `quality.h`'s mirrors in the same commit; -old caches are rejected and rebuilt. +- **One cold parse.** The parser version moves to 93 (#139), then 94 (#172), then 95 + ([#207](https://github.com/redhat-et/ripwire/pull/207)); the cache format moves from 20 to 21 (#139) and stays there. + `loadCache` returns empty on a version-or-parser-version mismatch, so the first run after upgrading reparses the tree + and rewrites its cache. The quality snapshot scheme moves from 10 to 11 (#207), so the first `--quality-delta` after + upgrading recomputes its snapshot. + The parser version moves once more, to 96, and the cache format from 21 to 22, for the internal-linkage bit on + every C and C++ definition ([#216](https://github.com/redhat-et/ripwire/pull/216)); a cache written by 0.6.0 is + rejected and rebuilt on the first run either way. +- **A sidecar must be a regular file: a symlink at a sidecar name is refused, on read as well as on write.** + `.ripwire_notes`, `.ripwire_quality_baseline` and `.ripwire_arch_baseline` are opened with `O_NOFOLLOW`, so a + link at one of those names is not opened, wherever its target is. Anything else at the name that is not a regular + file, a FIFO for example, is refused as well instead of being waited on. If you symlinked one on purpose (into a + shared config directory, say), replace the link with a regular copy of its target. Until you do, every read of it + prints a refusal on stderr, no notes surface, `--quality-delta` reports `baseline="git-HEAD (symlinked sidecar + refused)"` and compares against HEAD, `--arch` reports every violation as new, and `--note-add`, + `--quality-baseline`, `--arch --baseline` and `--baseline-update` exit 1 without writing. `.ripwire_config` and + `.ripwire_quality_acks` are unchanged (#178, [#191](https://github.com/redhat-et/ripwire/pull/191)). +- **New flags and flag behaviour.** + - `--in=DIR` is new, on the default map's churn-decay branch only (`--rank-by=churn-decay --in=DIR`). DIR is + root-relative and must exist under the root; absolute paths and `..` are refused. Under `--in`, the symbol map + collapses to a `` stub and a second + `` block follows the global one, which stays byte-identical. `--in` + joins the house `--offset=`/`--limit=` paging set; it is refused, naming the remedy, with any other verb, with + multi-root, with `--top-k=0` and with `--json` — and, since the CI round, refused rather than silently ignored + when a report verb wins dispatch (#212). + - `--for=TASK --limit=N` no longer means what it meant: it now serves a file-grain widening page, one + `` row per positive-score file, paged with `--offset=M`. `--top-k` stays inert on `--for`, + and `--help` now says which flag widens. Beside the page every bundle-shaping flag is refused, never ignored + (#213). + - `--legend=compact` is what the generated agent commands now ask for — the skills, the `ripwire wrap` paste block, + the prompt routers and the tool routes. Add `--legend=full` to any of them to get the full legend back; the MCP + `legend` argument's schema description now says so too. The bare CLI default is unchanged (#215). +- **Output that changes by design.** Each change is described in its entry below. + - **`sc=` replaces `id=` on map and lens symbol rows.** A scoped row carries `sc=`, the enclosing scope; the full id + composes as `p::sc::n`, with `p=` read from the row or from its `` wrapper, and the legend states that + composition. **Every selector still accepts the composed `id=` spelling on input** — `--expand`, `--callers`, + `--impact`, `--uses` and the MCP twins are untouched. `` rows, `--expand`'s whole-file anchors and + `--merge-scout` rows keep `id=`, because their path does not repeat on the row. `--json` twins print `"sc"`. + `route=` on `--for` becomes a code rather than a sentence, and same-named callees of one `calls` block merge into + one `` row (#215). + - **`` grouped test rows.** A consumer that parses `tests_to_run` must learn one new row shape: contiguous + runner-less rows whose other attributes are byte-equal are served as a single `` row in XML, as one object with an array `"p"` (or `"test"`) in JSON, and as one + `[hops=N] (n): a, b, c (run: not derivable)` line in `--situ` text. Rows that carry a runner stay single + ``/`` rows, a group of one stays a single row, a `,` inside an XML path is `,`, and every path is kept + verbatim — the multiset of paths is identical before and after (#214). + - A `file:name` selector on a C++ header declaration keeps only the definitions tied to that header, so `--callers`, + `--callees`, `--impact`, `--safe-delete`, `--path`, `--uses`, `--mentions`, `--verify` and `--affected` can answer + fewer rows. They carry `unproven_defs=` when they dropped any (#173, #190, #195), as do `--edit-check`, `--lego`, + `--connect`, `--around`, `--slice`, `--expand`/`--outline`, `--owners`, `--note-add` and the MCP twins (#210). + - A C/C++ declaration without a body yields the focus to the lowest-id C/C++ definition with a body in the same + scope; every other case keeps the lowest id. Four legend sentences that called the pick "the lowest-id one" are + reworded (#210). + - `--callers` on a narrowed selector with declined calls points `next=` at the bare-name `--uses` call, and its legend + says so (#182). + - `--lego`, `--verify` and `--nonlocal-state` define `graph_unindexed=`. `--legend=compact` answers define the + attributes they print, so some compact answers are larger, and each compact schema is now pinned at its own + measured size rather than at one 400 B pin: map 810 B, communities 820 B, map-diff 800 B, impact 780 B, + community 730 B, safe-delete 720 B, around 720 B, metrics 720 B, pack-signatures 680 B, pack-top-n 660 B, + query 630 B, and the remaining schemas between 140 B and 410 B. `--help` states the sizes and, restated from + measurement, a saving of "at least 45%" rather than "at least 50%"; the measured savings on a small answer are + `--callers` 65.91%, `--uses` 63.79%, `--impact` 46.17% and `--affected` 64.73%, a per-call drop of 2.8–5.8 KB + (#169, #185, #189, #203). `--for`'s own compact dialect is present-only and pinned at 690 B (#215), and the + pack-task schema moves to 880 B where a fixture's runner-less rows now define `run_unknown=` (#214). + - A budgeted `--for` that takes rung zero names the legend definitions it dropped (#174). + - Every churn-decay `` block carries `merge_bombs_skipped=`, `"0"` included, and an all-bomb window prints + `` where it printed no block at all (#212). + - Records inside a literal `#if 0` no longer serve any role: reads, writes, imports (`using ns::x;` and `#include` + alike), `extends`, types, `#else` branches, variable-to-type bindings and definitions are all excluded, where 0.6.0 + excluded only calls. `--uses` counts can fall, `amb=`, `prov="split"` and `overloads=` can lose rows minted by code + that cannot compile, and `--expand=deadType` refuses with a suggestion instead of serving a dead body. `--grep` is + unchanged: text inside `#if 0` is still findable (#172). + - `--connect`'s `est_tokens=` reads higher on a tree that has an unindexed file (#171). + - `--help` lists twelve flag rows it had left out, and the `--scip` help row says a missing index refuses (#170, #184). + - The map header, `--skipped`, `` and `` can carry `escaped_root=` (#179). + - `--scip` naming an empty file, a directory, a FIFO or a device exits 1 (#197). + +### Added — `--for` pages its answer one file per row, and says when to widen + +On the pre-registered follow-up ladder (a 2,066-file C++ corpus pinned at one commit, the frozen 30 questions, six +deterministic steps per tool, no model in the loop), every ripwire follow-up completed 0 answers through step 4: +`--for`'s `next=` pointed at `--expand` (a body, not a wider list), `--top-k` was inert on `--for`, and +`--format=candidates` is symbol-grain (40 symbols is about 18 files in 11 KB). The one follow-up that completed answers +in that ladder was a file-grain page — one row per file, about 6 KB. Local telemetry had `--for` → `--expand` followed +0 of 259 times. + +`--for=TASK --limit=N` (`--offset=M` pages it) is now that page: a `` document of one `` +row per positive-score file, `p=` spelled root-relative exactly as every other verb spells it, ranked file-first by +`score=` — the IDF-weighted share of the query's subtokens the file's top 8 symbols cover between them (a term counts +once however often it recurs, so one huge file cannot monopolise; ties by the best symbol's lens score, then path). +The root carries the house paging vocabulary (`shown= total= capped= has_more= next_offset= offset= limit=`) and a +`next=` naming the next page. When the answer is THIN — the top-ranked symbol's name, doc or body carries under 50% of +the query's IDF-weighted subtokens (an unmatched subtoken weighs as the rarest, so a `(#12147)` token lowers the share +honestly), or the ranked head spreads over fewer than 3 files — `--for`'s root carries `coverage=` (that share, whole +percent) with its legend clause, and the r=1 row's `next=` names `--for=TASK --limit=40` instead of the body. A +confident answer carries none of the three and is byte-identical to before; the `--json` and MCP twins follow the same +present-only rule. The MCP `for` twin takes the same `limit`/`offset` and serves the same page through the same +renderer. Beside the page every bundle-shaping flag is refused, never ignored (`--limit=0` and non-numeric values were +already refused). `--top-k` stays inert on `--for` and `--help` now says which flag widens. + +Measured, on the ladder re-registered with the page as step 2 on the `--for` shapes: ripwire's complete@step row is +unchanged at 14/14/14/14/17/17 — the page completed no question, because the seven misses it ran on hold 3–21 gold +files each — while adding gold files on four of the seven (+2, +1, +3 and +6 files) at 5,539–6,212 B per page (mean +5,841 B), and the thin rule named the page on 4 of those 7 misses. The frozen-30 single-call instrument is unchanged at +14/30 complete and 42/129 gold files named; its median bytes-to-answer is 6,348 B (5,988 B before: 10 of the 12 +`--for` questions on that instrument are thin — commit subjects with a `(#NNNN)` token, "how does A reach B" questions +— and carry the clause; the 2 confident ones read the base again, and the 18 non-`--for` questions moved by the 2–4 B +the git stamp moved on every verb). Gate: `test/forwidencheck.sh` — a generated 33-file fixture whose gold file sits at +page rank 13 and is absent from the default head and tail; one row per file, determinism, paging with no overlap, +`coverage=` defined in both dialects, thin versus confident `next=`, the refusals, MCP parity — red on the pre-change +binary. The byte pins that ride a thin `--for` header (forrankordercheck's fixture rows, forrootlegendcheck, +compactlegendcheck's loop, the two `--no-route` goldens) were re-anchored with the measured number; the confident ones +read the base again ([#213](https://github.com/redhat-et/ripwire/pull/213)). + +### Added — `--in=DIR` scopes "what changed recently", and the churn window discloses the merge bombs it skipped + +Three defects, one lane. + +**The churn window hid the commits its merge-bomb rule skipped.** The decayed git walk skips any commit touching more +than 100 indexed files and counted nothing about it, so a `` block could omit the very commit a question was +about — a held-out gold commit touching 71 source files was invisible — with no trace in the output. Every churn-decay +block now carries `merge_bombs_skipped=`, `"0"` included; the threshold is the named `kChurnMergeBombMaxFiles = 100`, +listed in `docs/LIMITS.md` and `static_assert`-pinned to its legend text, and defined in both the full and the compact +dialect. A window in which every commit is a bomb — a shallow clone of a large tree; llvm-project at depth 1 is one +183,835-file commit — used to print no `` block at all, which made the new count vanish on exactly the run that +needed it; it now prints ``. A tree with no git still prints no +block. + +**There was no directory scope for "what changed recently in DIR".** `--rank-by=churn-decay` answered with a +whole-repository symbol map plus one global `` block that a directory with more than 40 recently-touched +files never fits into, and the sub-root workaround loses the global block and spells `p=` sub-root-relative. +`--in=DIR` keeps the global block byte-identical, adds a second `` +block built from the same mining pass and spelled on serialize's own root-relative rule, pages it with the house +`--offset=`/`--limit=` (40 rows, then `capped="1"` and a `next=` carrying the page verbatim), and collapses the symbol +map to a `` stub. + +Measured (RocksDB at `0e2801ac`, read-only corpus, scratch cache, warm, `wc -c` on stdout): the bare +`--rank-by=churn-decay` answer is 39,813 B; `--in=db` is 10,241 B, `--in=util` 10,165 B and `--in=table` 10,711 B. +The saving is the stub — 68 B in place of the 200-row map — and the scoped block itself *costs* 2.2–2.75 KB per +answer; RocksDB reads `merge_bombs_skipped="30"`. On this repository, `--in=src` takes 46,843 B to 9,259 B and reads +`"5"`. The compact legend grows 1,783 B → 1,939 B under `--in=` (+156 B: `scope=`, `total=`, the window terms). On +llvm-project (183,835 tracked files, `/usr/bin/time -l`, scratch cache, two warm samples each) the per-file scope pass +costs nothing measurable: warm bare and warm `--in=llvm/lib/Analysis` both run 2.41–2.44 s real at 2.2 GB max RSS, +while the answer falls from 47,967 B to 5,676 B. Honest caveat: that clone's single commit is a merge bomb, so every +file weight is 0 and the prefix predicate short-circuits — the 183k-file loop is exercised, but the path compare is +exercised at scale only on RocksDB's 1,857 touched files. + +Gates: `test/recentscopecheck.sh` (58 PASS, 42 arms red on the pre-lane binary) and `test/churndecaycheck.sh` arm 7 +(red on the pre-change binary: no attribute anywhere), both also clean under ASan +([#212](https://github.com/redhat-et/ripwire/pull/212)). + +### Added — the task router reaches the recency question, and this round's shapes are named where an agent reads + +Three defects, one lane. The framing for the round: for this to work the whole system has to be put together — the +answers need shortening, but the agent also has to know how to use them. + +**The recency question routed nowhere.** `what changed recently in db`, `who touched this lately`, `the newest commits +here` — every phrasing abstained with `score="0"`, so `--rank-by=churn-decay`, and the `--in=DIR` scope landing beside +it, could not be reached from a task said in words. The cause was simply that no churn or recency intent existed. +`recent` is on `kWeakSymbolStopWords`, but that list governs symbol resolution only — it is why `--expand='recent'` +can never be minted out of prose — and it has never had any bearing on which INTENT a task reads as. Nothing came off +the stop list, since those words must still never name a definition; they became intent evidence instead, which is +what the list's own comment says they are, and the correction is recorded in the source beside the new route so the +next reader does not re-derive it. + +The new `recency-window` route is CONJUNCTIVE in three parts, because two are not enough: a TIME word, a MOTION word, +and a word naming the corpus or a directory of it the task named. A time word alone is usually part of a compound noun +(`the recent-file cache`); a time word and a motion word together is also a sentence about a supplier revising their +terms last quarter. An EXPLANATORY question is never this route however many of the three it holds. Cues are matched +word-bounded — the explanatory ones included, since a phrase delimits its interior and nothing at its two ends, and a +word ending in `how` followed by one beginning `do` is how `show documentation` swallowed a cue. The route runs LAST, +only when the weighted tier named nothing, which is both the argument that it costs the older routes nothing and how +it reads a dirty worktree: `is my diff safe to merge, i changed these files recently` is still `review-diff`'s +question, and `review-diff` wins before this route is reached. + +**`--in=DIR` is composed only when both halves hold.** The task must name a directory of the CORPUS in a locating slot +— the same cue discipline the symbol slot uses, matched on `rw::sarif::rootRelativeUri`, the one root-relative +spelling the map's `p=` and `--in=` both use — and the running build must ship the flag, which `cli.h`'s new +`shipsViewFlag` reads from the flag table itself. A router that composes a flag its own parser has no row for hands +back a command that exits non-zero on the first paste, which is the prerequisite violation this file refuses from +every other direction. When the task names a directory this build cannot scope to, the `reason=` says so instead of +handing back a whole-repository answer to a question about one directory with nothing marking the drop. The directory +walk takes the EARLIEST slot in the sentence rather than whichever cue sits earlier in an array. + +**The widening page was discoverable only from a thin answer** — one call too late for an agent choosing what to run +FIRST, and the first call is what `--help-task` exists to pick. Every `--for`-shaped recommendation now carries +`next="… --limit=40"` on its ``, keyed off the INTENT rather than off finding `--for=` anywhere in the command +text (a task that quotes the flag inside another verb's argument had handed `--pack-task` a page width it refuses, +measured: exit 1), and spelled by `forpage.h`'s own `forWidenNext`, so the recommendation's follow-up and the answer's +own follow-up are one spelling under one 120-byte ceiling rather than two spellings at 197–236 B. Present-only: a +recommendation that is not `--for`-shaped carries no attribute at all. + +**`--help-task` had no legend in the default dialect.** Every attribute a reader met on its only screen was undefined, +with the compact layer's present-only legend the only place any of them was explained. One line defines all twelve +plus ``, ending in the shared `kNextLegendClause`, and `--help-task` joins `legendcoveragecheck`'s enumeration +and `nextverbcheck`'s population. + +**No new shape of this release was named where an agent reads.** Measured on main's skills, `--for`'s +`--limit`/`--offset` page and its `coverage=` gauge appeared in no skill body and no wrap primer within reach of the +verb they belong to: `--limit` is named, `--for` is named, in different files, and naming the two apart does not tell +anyone the page exists — the PAIR is the instruction. Four skills gain one or two sentences each, no frontmatter +touched. `ripwire-fresh-eyes` gains the history question (`--rank-by=churn-decay`, `--in=DIR` and what it does to the +map, `merge_bombs_skipped=` read as the disclosure it is, `scope=`); `ripwire-orient` gains the thin-answer rule +(`coverage=`, and that the step after a thin answer is `--for=TASK --limit=40`, not a body) and how to compose a +selector out of a row whose identity is `sc=` (`p::sc::n`); `map-before-you-read` gains the same in its pagination row +(on `--for` a `--limit` is not a cut but a wider net, and the bundle-shaping flags are refused beside it); and +`ripwire-change-check` gains the grouped `` row beside `--affected`, with the +invariant it preserves. + +Measured (`bench/taskroute_eval.py`, the committed content-hash split; only the binary and the corpus change between +rows): + +| stage | binary | rows test / dev / all | accuracy test | dev | all | precision | harmful | +| --- | --- | ---: | ---: | ---: | ---: | ---: | ---: | +| before the lane | `origin/main` | 114 / 111 / 225 | 0.939 | 0.946 | 0.942 | 1.000 | 0.000 | +| after the first review round | `aaa3baa6` | 128 / 119 / 247 | 0.945 | 0.950 | 0.947 | 1.000 | 0.000 | +| this head | `342d16d9` | 130 / 119 / 249 | **0.946** | **0.950** | **0.948** | 1.000 | 0.000 | + +Coverage at this head is 0.917 test, 0.933 dev, 0.925 all, and **every miss is an abstention**: `precision=1.000` and +`harmful=0.000` on all three splits, with `want=… got=abstain` the only confusion row, and negative specificity 1.000. +The control is this head's binary scoring the pre-lane 225 rows — 0.939 / 0.946 / 0.942 at coverage 0.907 / 0.929 / +0.918, the same three numbers as the first row, so no inherited row moved. The lane added 24 rows (225 → 249): ten +positives, four of them naming a directory, and four decoys in the first round; then four NEGATIVES, one per +word-boundary class a review found (`here` in where/there, `source` in outsource, `file` in profile, `code` in codec), +one for a dirty working tree, three positives for vocabulary that abstained (a verb below the motion floor, +`since `, `what is new in DIR`), and two for the cross-word explanatory cue. Two of the review rows +are labelled `instrumented-cli` rather than `handwritten`, by the rule the corpus's own section states: their trigger +is a small closed phrase list, so a sentence that routes necessarily reuses one of its phrases. + +Two claims an earlier revision of this lane made were WITHDRAWN by review, and are corrected in +`test/taskroutefix/PROVENANCE.md` rather than left standing. That the 225 pre-existing rows are byte-identical on +(status, intent) across the new route is true and nearly vacuous — measured, 0 of those 225 prompts reach the recency +route at all, so the identity was never in question, and a number that cannot move is not a measurement; the evidence +that the route steals nothing is the corpus's own negatives and the gate's arms. And the contamination screen is not +down to one flagged line from two: measured with one binary at three points it reports the same 2 flagged lines every +time, neither of them a row this lane wrote. + +Gates. `test/taskroutecheck.sh` gains 11 arms in the first round and 20 more in review; against the pre-change binary +4 FAIL — both recency phrasings answer `status="abstain" … score="0"`, and the locate-task recommendation carries no +widening `next=` for the follow-up arm to recover. The emitted commands are EXECUTED, not merely matched: the bare +recency command must return a `` block, and the widening `next=`, unquoted with `shlex`, must return the +`` page. Every arm reads the COMMENT-STRIPPED body, so a legend that names an attribute can never satisfy an +assertion about a row carrying one. `test/agentsurfacecheck.sh` is new, and red against main's skills with 3 FAIL — +no skill body or wrap primer named `--limit=`, `--offset=` or `coverage=` within five lines of `--for`, so the +file-grain page and its thin-answer gauge were unreachable from a skill. Its arm (A) is a RATCHET over all 163 long +flags `--help` advertises: each is named in a skill body or the `ripwire wrap` primer, or recorded in +`test/agentsurfacefix/unnamed_flags_baseline.txt` with the reason it is still a gap (5 lines today: `--eval-skills`, +`--eval-stray`, `--pin-census`, `--max-file-size`, `--sarif`), a floor that may only be edited DOWNWARD and that also +fails when a recorded line stops being a gap, so a closure cannot be filed and forgotten. The match is word-bounded, +because 23 advertised flags are a strict prefix of another (`--in` inside `--index-out`, `--not` inside `--notes`, +`--for` inside `--format`) and a substring test would report every one of them as named by its longer sibling; the arm +prints that count, so the population the bounded match protects is visible. Arm (B) pairs each of this round's new +shapes with its verb within five lines on one surface, PROBED by RUNNING the verb — `--help` advertising a flag is not +evidence that the flag emits anything — with what the binary emits (`` trap that deletes bare `__restrict` in C++ — - `__restrict__` is the only spelling allowed in `src/`. `test/noaliascheck.sh` (eight arms, red against the old - definition) proves it. The optimizer half is a separate switch: BasicAA reads the bundle only when - `basic-aa-separate-storage` is on — `cl::init(false)` in LLVM 17 (AppleClang 16 / Xcode 16.2: the macos-14 CI - runners and the macos-arm64 release leg), `true` from LLVM 18 — so CMake now probes and passes - `-mllvm -basic-aa-separate-storage` to our targets (and to the ld64 link under LTO), and the gate classifies the - compiler by compiling the real slice three ways, with a `=false` negative control and a cross-check against the - cached CMake probe. -### Added — `VERIFY_NO_ALIAS` guards at 15 call sites where self-aliasing was a silent wrong answer or UB - -`VERIFY_NO_ALIAS` / `VERIFY_NO_ALIAS3` at the top of 15 functions whose two-or-more same-element-type -out-parameters would silently mis-compute or invalidate an iterator if a caller ever passed the same -object twice. The check runs in debug builds; in release the macro leaves only the -`__builtin_assume_separate_storage` promise on the two objects, which the optimizer reads on clang 18+ -by default, on LLVM 17 / AppleClang 16 only with the CMake-added `-mllvm -basic-aa-separate-storage` -and there for scalar accesses, and not at all on GCC or clang before 17. For these 15 functions the -promise measured no codegen change (the object form says nothing about a container's heap buffer), so -there is no performance claim here: these are correctness contracts. -- **Six more aliasing contracts at function entry, completing the audit; one of them is the tree's only codegen row.** `waterFillRecallShares` - (`src/recall.h`) reads `demand[i]` while writing `alloc[i]` and never resizes either, so it takes the buffer form: - release codegen 309 → 301 instructions under the build's own flags. `splitNoteTail` (`src/notes.h`), - `takeAckNamedToken` and `computeDelta` (`src/quality.h`) take the object form, whose check runs in debug and whose - release residue is the `separate_storage` promise on the two objects (read on clang 18+ by default, on LLVM 17 / - AppleClang 16 only with the CMake-added flag and for scalar accesses, never on GCC or clang before 17); for these - it measured no codegen change. `computeDelta`'s two out-pointers both default to null, so its guard is a - null-safe `VERIFY_TEXT` rather than the object form. - `markCandidateFilesIncludingDecl` (`src/graph.h`) and `partitionByScope` (`src/verbs_quality.h`) take the last two - guards of the audit's apply list, which is now complete: 21 functions state their no-alias contract at entry. +out-of-tree row (12 → 13). `kParserVer` 92 → 93 with the mirror (the branch spent 89 while main spent 89..92 on the +extent detector, Kotlin and the yaml patch); cache format 20 → 21 (`Include::isValueUse`); re-pins with reasons +in-file: `test/qschemetrip.hash`, `test/printf_parity.manifest` (the `--impact` help and legend name the two new +closure kinds; the `--deps` legend's lazy definition gains the rescue class). `docs/COMMANDS.md` regenerated +(2026-09-11). + +Contributed by **@andriytyurnikov**, round three of their Ruby constant work +([#139](https://github.com/redhat-et/ripwire/pull/139)). + +### Changed — short symbol ids on maps, a compact legend on `--for`, and compact by default on the agent surfaces + +**`sc=` on symbol rows.** Every scoped symbol row printed its canonical id in full — +`id="src/mcpverbs.h::rw::applyCompactToBatchSubs"` under an `` wrapper that had just printed the +path, or beside the row's own `p=` on a lens `` row. The row now carries `sc=`, the enclosing scope, the one segment +nothing else on the page holds; the legend states the composition `p::sc::n`, and **every selector still accepts the +composed spelling** — the resolver is untouched, and a new `test/scroundtripcheck.sh` (17 arms) proves the composed +multiset equals the old `id=` multiset. `route=` becomes a code (`name-exact(X)`, `subtoken+body`, `:broad`, +`:declined(word;carriers,defs)`) with one shared spelling for the CLI lens, the MCP `for` twin and the compact dialect, +so a code cannot acquire two readings; the fuller reading lives once in `--help`'s `--no-route` entry. Same-named +callees of one `calls` block merge into ``, and `shown=` still counts callees. + +Measured (`wc -c` on stdout; a build of the merge base run on the same merged tree, so no corpus drift rides the +numbers): this repository's flagless map falls from 26,449 B to 22,407 B — **−4,042 B, −15.3%, the same rows** — +`test/cppqualfix` −5.2%, `test/nestedqualfix` −5.3%, and `test/accessshapefix` **+9 B**, where four scoped rows do not +pay for the longer reading. On `--for` the bundle is byte-shaped, so the row saving becomes rows rather than bytes: +`pagerank power iteration` goes from 9,470 B at `shown="20"` to 9,880 B at `shown="25"`, and `rank graph teleport` from +10,134 B at 19 rows to 10,022 B at 22 rows. + +**`--for`'s compact legend is present-only.** Every other XML verb under `--legend=compact` answers with a present-only +legend pinned per schema; `--for`'s native compact dialect was the default sentences behind a schema id (1,177–1,216 B +on the gate's fixture) and exempt from the pin by name. It is now one present-only comment — a reading per attribute +the bundle actually prints — measured by the gate's own splitter at 678 B on the fixture probe (915 B before) and +pinned at 690 B as the `ripwire.for/v1` row. Per call on this tree, default legend → compact: `pagerank power +iteration` −537 B, `rank graph teleport` −309 B, `escapeXml` −1,222 B. + +**The agent surfaces ask for the compact legend, and say how to get the full one back.** Every command ripwire writes +for an agent carries `--legend=compact` where the verb accepts it: 158 `ripwire ` verb commands in 17 skill files +(bodies only — no description, stop rule or boundary moved), 9 commands in the `ripwire wrap` paste block, 26 +`--help-task` routes and the 2 tool-call routes. One sentence per surface, and not seventeen, says to add +`--legend=full` when a definition's reasoning is needed: the wrap blurb, one new section in the router skill's shared +conventions, a parenthetical in the three route hooks' injected context, and the MCP `legend` argument's own schema +description. `--for` keeps the default legend; the text, JSON and writer verbs are untouched; the bare CLI is +unchanged. Separately, the `--observe` arm of both prompt routers now counts a call only when the command word really +is the binary, so `cd …/ripwire && git log --oneline` no longer burns an adoption-window slot. Gate: +`wrapverbscheck` arm 8, six rows — the wrap blurb, the router skill, the three route hooks and a live `tools/list` over +`--mcp` — verified red against the merge base's tree and binary, 0 hits on every one of the six +([#215](https://github.com/redhat-et/ripwire/pull/215)). + +### Changed — tests-to-run rows without a runner are grouped, and the disclosure is stated once per group + +A tests-to-run row with no derivable runner said so on the row — `run_unknown="1"` (XML, 16 B), `"run_unknown":true` +(JSON), ` (run: not derivable)` (`--situ` text, 23 B). On a corpus where almost no harness has a derivable runner +that is one fact repeated per row: on RocksDB, `--affected=db/write_batch.cc` lists 127 tests, 126 of them runner-less, +and spent 2,016 B of XML and 2,898 B of text on the repetition. The disclosure is right — an absence is not a +disclosure — its per-row placement was the cost. + +Rows already come in evidence order, so a contiguous run of runner-less rows whose other attributes are byte-equal is +served as ONE `` row, emitted where its first member stood. Rows that carry a runner stay single rows, a group of +one stays a single row, a `,` inside an XML path is escaped `,`, and **every path is kept verbatim**: the multiset +of paths before and after is identical, and the emitted order is preserved by construction, because a group covers a +contiguous run only. All twelve emitter sites in nine files render through one seam, and the legend clause is spliced +rows-gated, so a `tests="0"` answer pays nothing for it. + +Measured (RocksDB, read-only corpus, scratch cache, same commit, `wc -c` on stdout), on the 127-row +`db/write_batch.cc` list: `--affected` 10,668 → 6,878 B (−35.5%), `--test-gate` 13,242 → 9,633 B (−27.3%), +`--test-gate --json` 11,055 → 7,163 B (−35.2%), `--situ` 11,769 → 7,357 B (−37.5%), +`--affected --legend=compact` 9,312 → 5,313 B (−42.9%) and `--test-gate --legend=compact` 11,223 → 7,596 B (−32.3%). +Eight `` rows replace 124 single rows; three rows stay single. The bytes still spent on the disclosure after +grouping are 160 B of 2,016 B in XML and 230 B of 2,898 B in text. A three-row list pays rather than saves +(`--affected=cache/tiered_secondary_cache.cc` 2,352 → 2,690 B: that verb had never defined `run_unknown=` at all), and +on this repository, where every harness has a `.sh` runner, nothing groups and the deltas are the legend alone +(`--affected` +371 B, `--test-gate` +180 B, `--situ` +79 B). `--pack-task="change WriteBatch::Put"` serves +`` where it served `shown="28"`. + +Gate: `test/testrowruncheck.sh` arm 12 (the multiset-of-paths and order invariants in all four dialects on a fixture +with three hop groups and a runner-bearing row inside one) red on the pre-change binary, plus arm 13 sweeping +`--token-budget` 1000..1700 to prove a byte cap can no longer drop two paths that each fit alone. Two new +`prcontextcheck` arms hold the third finding: the run clause is now priced and written from the RENDERED body, so a +bundle whose selected range reaches no test cannot buy the clause +([#214](https://github.com/redhat-et/ripwire/pull/214)). + +### Changed — one absolute root per change report, `--situ`'s disclosures become gauges, and a changed file's lexical siblings + +Three items from the output-routing loop's list, one commit each, stacked on the grouped test rows above. + +**A runner command pasted the whole checkout prefix on every row that had one.** A change report states its absolute +root once, in the envelope, and every path below it is relative to that root — which is what makes the document +independent of where the tree is checked out. One emitter never joined: `testmap.h`'s `spell()`, which builds the +`run=` command, pasted the disk path verbatim. On an absolute root `--test-gate` printed the checkout prefix three +times — the `root=` anchor, `next=`, and every `` row's `run=` — and `--situ` once per runnable test line, a +per-ROW cost against a per-DOCUMENT fact. The sweep could not see it: `test/fixture` holds no runner script at all, so +every test row there reads `run_unknown="1"`, and the one emitter that pastes a PATH INSIDE A COMMAND was never +exercised. `TestRunnerIndex` now takes the run's crawl root and spells the command through the same +`rw::sarif::rootRelativeUri` every `p=` beside it uses, with the hand-rolled leading-`./` strip becoming that one +call; the root is passed at all fourteen construction sites, so the twelve emitters sharing the index cannot disagree. +**The relativity is gated to single-root runs.** A multi-root run, whose disk path is under no single root, keeps the +absolute command, because an unrelativizable command must stay pasteable rather than become relative to a root that +does not contain it — and the spelling and the sentence that describes it now answer to ONE predicate, +`runsAreRootRelative`, read by the index and by all eight legend sites, so the clause can no longer tell a multi-root +reader that a command is relative to a root the document never names. `kRunHintLegendClause` gains the sentence +(rows-gated, like the rest of that clause), `--situ`'s `[2]` header says a `(run: …)` is relative to `root:`, and +`--help` and the regenerated `docs/COMMANDS.md` say what the code does — including the multi-root exception — where +they had said `run=` is spelled with the same root you scanned. Two more surfaces that hand a caller something to +PASTE gained the anchor they lacked: `--flags --flip` emitted root-relative `p=` and declared no root, and the MCP +edit receipt had relative `file`, `run` and `next:` and no root; both carry `root=` now, single-root only, with the +one sentence that defines it. `rootRelativeUri` itself returned on a leading `./` before it tried the root prefix — +right for the root `.`, wrong for every other relative spelling: `ripwire ./corp` stores `./corp/test/x.sh`, the early +return yielded `corp/test/x.sh`, and pasting that from the declared root is rc 127. Both sides drop the optional `./` +first and compare what is left; the root `.` case stays byte-identical. + +**`--situ`'s disclosures are gauges, and every gauge keeps its reading.** `--situ` is the only report with no XML root +to hang attributes on, so every disclosure it owed was a sentence, and the sentences grew: a floor clause, a decl/def +partner header, a tests-to-run header and a script-gate caveat, about 1.2 KB of prose per call carrying facts a reader +can act on only once they are named. Each is now an attribute line, spelled as the XML and JSON dialects already spell +the same fact, so the three share one vocabulary: `counts_floor=1` beside `graph_ambiguous=`, `graph_unresolved=` and +`graph_unindexed=`; `not_dependents=1`; `prcontext_cap=20`; `order=evidence`, the attribute `--affected`'s root +already carries; and `script_gates_unmodelled=`, the counter `--affected` publishes. Nothing is dropped — every floor, +cap and caveat survives, and the two readings with no attribute form, how to read a zero and what +`[changed]`/`[partner]`/`hops` mean on a row, stay as the shortest sentence that defines them. **An attribute without +a reading is a token, not a disclosure**, so each gauge keeps a short gloss: the floor's CAUSE (call edges are +name-based), what an unindexed file IS, which header the resolver gauges come from, and whose cap `prcontext_cap=` is. +`--situ` refuses `--legend=compact` and is the one dialect with no legend to look a name up in, which is why the gloss +is not optional here. + +**The files a change drags with it were the ones no walk could reach.** The files that move WITH a changed file are +its neighbours by name, and the caller walk reaches none of them: a header does not call the source that implements +it, an `.inl` is not indexed by any grammar in any build, and a harness the graph cannot link is reached by nothing — +two answers on the frozen 30-question set were incomplete for exactly that reason. Section `[1]` now lists them under +the decl/def partners and the floor clause: same directory, and the same filename stem or the stem-partner convention +`testmap.h` already owns (`_test`, `test_`, `Test`, `_unittest`, `_spec`). Same directory is +load-bearing — a same-stem file in another directory is a namesake, and listing namesakes would make the block noise +on exactly the large trees it is for. The rule is **stricter than the design that simulated it**, an exact stem plus +the test-partner affixes rather than a shared stem TOKEN, so it lists fewer files and costs less; whether the stricter +rule still completes those two questions is for the re-measure, and no completeness claim is made here. The candidate +population is the CRAWL's, not the index's, so the `.inl`/`.ipp`/`.tcc` partner a C++ change most often has to edit is +named; the crawl's unsupported-extension row list is itself capped, which is the one way this list can be short of the +truth, and that is disclosed as `unindexed_rows_floor=1`. The floor is a property of the CANDIDATE LIST, so it is +recorded whenever that list was short and the block speaks at zero as well — a crawl cut that removed the only +candidate used to print nothing at all, the silent zero `docs/METHODOLOGY.md` §9 forbids. The block is capped at 8 +with `shown=`/`total=`/`capped=1` and a pasteable `next:`, raisable with `--limit`, and with no offset: `--situ=F +--offset=20` had printed `shown=0 total=9 capped=1` with a `next:` offering relief that cannot restore rows an OFFSET +removed, and `--offset=7` had dropped six rows silently. It is additive to the decl/def partners above it — +suppressing the overlap was tried and reverted, because it removed `widget.h` from "the siblings of widget.cc" to save +about 20 B. The MCP `situational_awareness` twin carries the same list as `siblings`, with a `siblings_total` that is +now the population and an explicit `siblings_capped`, emitted and never omitted, rather than the length of the array +beside it, which was a tautology. + +Measured (`wc -c`, same warm cache, same commit, absolute root; the tip of the lane this one is stacked on against +this head over the SAME tree, so each pair carries all three items together): + +| corpus | verb | before | after | +| --- | --- | ---: | ---: | +| this repository (root 131 chars) | `--situ=src/graph.h` | 4,448 B | 2,955 B | +| this repository | `--situ=src/situ.h` | 2,332 B | 2,040 B | +| this repository | `--situ=src/testmap.h` | 2,325 B | 2,033 B | +| this repository | `--test-gate=src/testmap.h` | 5,455 B | 5,247 B | +| RocksDB @ `0e2801ac` (root 66 chars) | `--situ=db/write_batch.cc` | 7,489 B | 7,376 B | +| RocksDB | `--test-gate=db/write_batch.cc` | 9,946 B | 9,868 B | +| RocksDB | `--affected=db/write_batch.cc` | 7,124 B | 7,113 B | + +Per item. The root spelling saves one echo per row that has one, less the 56 B the conditional root sentence adds, so +it grows with checkout depth and with how many rows carry a runner: two echoes of a 132-character root on this +repository's `--test-gate`, one echo of a 66-character root on RocksDB's. The four compressed `--situ` lines, measured +by `situshapecheck`'s own `${#line}` on this repository at `--situ=src/graph.h` (the partner header on the gate's +fixture, since `graph.h` has no decl/def partner here), go 601 → 344, 228 → 209, 233 → 220 and 167 → 132: **1,229 B → +905 B**. Said plainly, that is about 20% less than the byte attribution predicted, because the prediction assumed the +gauge names could go unglossed and they carry a gloss instead — and it supersedes this lane's own first figures, which +were measured before the readings were restored and on a different corpus than the gate's. The sibling block costs +what it lists: **276 B** on RocksDB at `--situ=db/write_batch.cc`, a 244 B header and one 30 B row naming +`db/write_batch_test.cc`, which no other section of that report reaches. + +Gates. `test/situshapecheck.sh` is new and red on the base binary with 17 FAIL rows — the floor line 601 B over its +ratchet, the partner header 228 B, the `[2]` header 233 B, the four missing attributes, the whole sibling block, the +offset arm's premise, both silent-zero arms and the MCP twin arm. Its sibling fixture is a +`.h`/`.cc`/`_test.cc`/`.inl` quadruple, a same-stem DECOY in another directory and a same-directory different-stem +file (both of which must be absent from the block), a nine-sibling stem for the cap and for `--limit`'s relief, a +no-git copy of the same tree proving the block is static — which is also why it cannot leak — and the MCP twin +agreeing row for row; the silent-zero arm runs on a 700-`.inl` fixture that really does cut the crawl's 500-row +unsupported list, and asserts that premise before it asserts the floor. `test/rootrelemitcheck.sh` ARM 9 is red on the +unchanged binary with 8 FAIL rows and builds its own fixture with a real runner script at two checkout depths, +EXECUTING the printed `run=` from the declared root, because a relative command that cannot be pasted would be worse +than an absolute one; ARM 9b is a matrix over `.`, `corp`, `./corp`, `corp/`, an absolute path and a symlink, all of +which must print the SAME command, ARM 9c pins the spelling and the sentence against each other, and ARM 9d covers +`--flags --flip` at two checkout depths. `receiptpostcheck` (18) covers the MCP receipt, which (13) already holds key +for key against the CLI's, and `runhintcheck` 2c/2d the `--affected` twin. Two gate self-checks were wrong the same +way the code was and now red on that outcome: an empty `run=` made `eval ""` succeed, so `runhintcheck`'s execution +arm passed on the one outcome it exists to forbid, and `rootrelemitcheck` ARM 9's empty-`next=` case fell out of an +if/elif chain printing neither PASS nor FAIL. The suite also caught a dangling `string_view`: `runHintClauseIfRows` +BUILDS its clause now, because the root sentence is conditional, and `PackTaskHeaderParts` holds views, so binding +`runClause` straight to the returned temporary read freed memory — it showed as `packtaskcheck` reporting a bundle +that was both malformed and non-deterministic (two runs, two hashes) and `xmlwellformed` red on `--pack-task --json`. +Pins moved: `runhintcheck`'s nine expected values lose their root prefix, which is the contract change, stated; +`testgatelegendbudgetcheck` 3,000 → 3,070 B for the conditional root sentence (measured 2,957 → 3,013 B on its own +fixture); `situshapecheck`'s own byte ratchets 200 → 360 and 140 → 220, because a ratchet that forbids a disclosure is +aimed at the wrong thing; `printf_parity.manifest` for `pack_task` and `help_all`, the latter regenerated from the +merged binary's own hash because neither side of the merge was the answer; the gate count 612 → 614; and +`docs/LIMITS.md` and `docs/TUNING.md` regenerated for the new row cap, which takes the tree's cap inventory from 210 +to 211. `.ripwire_quality_acks` gains nine rows for `TestRunnerIndex`'s new root parameter and the eight sites that +pass it, and three more by symbol (`prLegendText`, `writeFlipHeader`, `runsAreRootRelative`); a duplication finding — +`situDirOf` was a 44-token copy of `siblift.h`'s `dirOf`, and `situStemOf` a fourth spelling of +`stripExt( baseNameOf( p ) )` — and a six-parameter new symbol were fixed rather than acked, and the default +`--quality-delta` reads `gating="0"` with no acks at all. ASan and LSan are clean on both fixtures and on `--situ`, +`--flags --flip` and the MCP twin, as are determinism and `xmllint --noout` on every changed verb +([#219](https://github.com/redhat-et/ripwire/pull/219)). + +### Changed — the declined-call index no longer grows with calls × candidates + +The call-graph build keeps, for every call the resolver declines to bind, the list of candidates it declined between, +so `--callers` and its neighbours can say how many calls were declined for a symbol. Those lists were stored once per +call, so the structure grew with calls × candidates: 27.9 M entries, 114 MB, on llvm-project, where most declined calls +repeat a handful of identical lists. Each distinct list is now stored once — keyed by its exact candidate sequence, an +FNV-1a hash picking the bucket and a hit confirmed by length and `memcmp` — as a CSR of distinct lists plus a call +count per list. On llvm-project that is 9,879 distinct lists and 62,359 entries: **368 KB instead of 114 MB**, with +peak footprint down about 145 MiB (median of 3 cold runs; the machine was loaded, so treat the timing and RSS figures +as indicative). Every count stays the same and the output is byte-identical to the merge base: 14 of 14 commands on +this repository, 14 of 14 on llvm-project, and `mcpclidiffcheck` 21 of 21. A uint32 offset overflow takes +`DEGRADED_PATH_ALERT`, and a new `verifyOffsetCsr` checks the list CSR beside `verifyCsr`. Gate: +`test/declinedlistcheck.sh`, 30 rows, of which a mutation that shares lists by name instead of by content fails 12 +([#208](https://github.com/redhat-et/ripwire/pull/208)). + +### Changed — the README and the docs + +- **The call for help.** The pitch now comes before the release line and the call for help + ([#167](https://github.com/redhat-et/ripwire/pull/167)). The call for help sits below the quality panel, where the + reader has already seen the tool work ([#183](https://github.com/redhat-et/ripwire/pull/183)). It says what to run + ([#175](https://github.com/redhat-et/ripwire/pull/175)) and offers a range of ways in: starter kits in + `prompts/help-wanted/`, three open-ended prompts (a full audit, adding a language, and logging every gap while using + ripwire on a real task), and open directions for research of your own + ([#181](https://github.com/redhat-et/ripwire/pull/181)). +- **Who the output is for.** Under the four commands worth learning first, the README now says every command prints + compact XML sized for an AI agent to read, and that human-readable output is on the roadmap + ([#196](https://github.com/redhat-et/ripwire/pull/196)). +- **A solved kit says so.** `prompts/help-wanted/` lists `next-uses-bare-name` under Solved, crediting @antoleod's + #182, and the README no longer counts the kits by hand. The reference guide's `--scip` sentence now names every + path that refuses, not only a missing one ([#202](https://github.com/redhat-et/ripwire/pull/202)). +- **Just want to use it?** The top of the README now says what most people do: install it, then tell your agent to + use it. The reference guide is marked as optional detail + ([#204](https://github.com/redhat-et/ripwire/pull/204)). +- **The showcase deck is 33 slides.** The "What `--quality-delta` catches" slide is pulled until better examples replace + it, and the rebuilt deck states the current gate count ([#205](https://github.com/redhat-et/ripwire/pull/205)). +- **What's new** had not changed since 2026-08-30 and did not mention 0.6.0. It now says what the release changed and + points at this file, which is the record ([#177](https://github.com/redhat-et/ripwire/pull/177)). +- **The project's voice is written down.** `CONTRIBUTING.md` §7 says commit subjects state what was wrong and the number + is the punchline. Where style and the honesty rules disagree, honesty wins + ([#176](https://github.com/redhat-et/ripwire/pull/176)). +- **Lua `require`.** `docs/ARCHITECTURE.md` said a `require` is a plain call and a `.lua` file is never a dependency + node. Since parser version 81, a string-literal `require` that resolves to exactly one file adds an edge, and the + paragraph now states that contract ([#184](https://github.com/redhat-et/ripwire/pull/184)). + +### Changed — the README gains a reference guide + +A numbered, plain-language reference guide goes at the bottom of the README, with a pointer to it near the top. It +covers install, first use, command families, output format, the accuracy and disclosure rules, determinism, agent +integration, languages and limits. Every existing README line stays. Written by **@heliocipher** +([#168](https://github.com/redhat-et/ripwire/pull/168)), landed in [#192](https://github.com/redhat-et/ripwire/pull/192). + +### Fixed — the reference guide said things the binary does not + +@heliocipher's reference guide was verified claim by claim against a 0.6.0 build, and its flags held up: of the 82 +`--` tokens it named, the 72 that are ripwire flags all exist and are spelled as it spells them (the other ten +belong to `cmake`, `xmllint`, `graphify`, `skills/install.sh`, or are anchor fragments). What it got wrong it +mostly inherited from this repository. +**"Dynamic dispatch contributes no edge"** was the opposite of the truth — a virtual call emits one edge per +candidate in the receiver's inheritance cone, each `prov="split"` and counted in `amb=`; a four-class fixture +returns three edges, not zero, with CHA-lite correctly dropping the same-name method of the unrelated class. The +honesty section was understating the tool, which costs a reader's trust the way overstating it does. +**`MSVC 19.36+`** was offered as a supported compiler beside an operating-system row naming only macOS and Linux; +it is not a target, and the row now points Windows users at WSL2. **"18 task-shaped skill files"** was one of three +defensible counts of one directory — 17 routable (`skills/*/SKILL.md`), 18 `SKILL.md` files in all +(`skills/hermes/` holds a Hermes-native one), 16 activated for every agent (`ripwire-opt-remarks` carries +`audience: contributor`) — and the README stated two of them, neither labelled. **"208 compile-time caps"** is 210 +by `docs/limits_build.py`'s own derivation from `src/`. **"Directory symlinks are not followed"** understated the +limit: no symlink is followed, and a symlinked source file is not indexed at all, so a tree that reaches its +sources through links reads as if they were not there. **Exit code 2 was missing** from the exit-code table, which +is the one a CI script most needs — a policy gate fired (`--arch`, `--scan-skill`, `--quality-delta`), not an +unknown failure. And one sentence sent a reader to `--scan-skills` to check a single file, which is the directory +verb; the file verb is `--scan-skill=FILE`. + +Unstated, and now stated: `git` is a runtime dependency for the history-backed verbs, which refuse with exit 1 and +a named reason rather than answering thin — `--map-diff` and `--rank-by=churn` answer and disclose the uniform +fallback, `--dmm` and `--pr-context` return an explicit unavailable row. A git URL as the root is the one thing +that touches the network. The write verbs return a receipt — region, `blob_sha`, contract check, tests to run — +so an agent never re-reads the file, and their payload is a whole definition, signature included, not a braced +body. Test coverage is read from call edges out of indexed test symbols, so a shell suite that runs a built binary +as a subprocess is invisible to it (`harness=script`, `reaches=0`) — which is this repository's own shape. `--json` +is an allow-list of nine verbs. `--quality-panel` has a `strict` preset that drops the two families which reshuffle +on unchanged code. Several verbs stay single-root in a multi-root run. Section 3.2 now splits by why a reader is +there — `scripts/pgobuild.sh` for a binary to use, the plain tree for work on the tool — and scopes the +`NDEBUG`/`DEGRADED_PATH_ALERT` warning to the development tree, where it belongs. + +`--replace-symbol-body`'s one-line `--help` summary said it replaces a definition's *body*. It replaces the whole +definition, signature included, as its own long help already said and as both insert verbs say. An agent that +believed the summary sent a braced body and deleted the signature — disclosed in the receipt as +`post_check_unavailable`, not refused. + +`test/readmedriftcheck.sh` gains three arms, so these counts cannot drift again: **(J)** the skills count, pinned to +the routable set and to the install fold's "sixteen of the seventeen"; **(K)** the `--json` allow-list, harvested +from `--help=--json` and required to match the guide in both directions, so a verb that gains `--json` support fails +the gate until the guide is updated; **(L)** the cap inventory, pinned to `docs/limits_build.py`'s derivation. Each +carries its own mutation control. Arm **(B)** took `head -1`, and so pinned one of the *two* sites stating the flag +count — the reference guide's copy had been free to drift since it was written; it now checks every site and names +the line that disagrees. `CONTRIBUTING.md` gains the rule those arms encode — an advertised count is an enumeration, +and if a set can be counted more than one way the prose must say which set — and the stale-object build hazard, +which until now lived only in `CLAUDE.md` ([#217](https://github.com/redhat-et/ripwire/pull/217)). + +### Changed — published captures withhold the rename rows from the project's own history + +The naming-calibration demo in the showcase captures and in `docs/COMMANDS.md` no longer reprints the rename rows from +the project's own renaming. Each withheld block is replaced by one line that says how many rows it withheld +([#193](https://github.com/redhat-et/ripwire/pull/193)). + +### Changed — CI, the gate harness and internals, with no change to output + +None of this changes the binary's output. + +**The gate harness.** +- **A passing arm can no longer print FAIL.** Gates reported with `A && ok || no`, where `ok` is a `printf`. A blocked + write to a full pipe can be interrupted by SIGCHLD and fail with EINTR, and the `||` then printed FAIL for an arm whose + condition held. That happened on a macOS CI shard for #126. `ok()` now always returns 0 and records a failed write as a + failure of its own. Every single-line site of that shape becomes an `if`/`else`: 1,782 sites on the base commit, as + counted by `test/gateexitcheck.sh` arm (G2)'s scanner. `test/pargates.py` captures each gate into a regular file, + where a write never blocks ([#142](https://github.com/redhat-et/ripwire/pull/142)). +- **`dispatchordercheck`** compared two runs of `--whereis`, which scans every branch of the repository around its + fixture. A branch created between the two runs changed the answer. The gate now builds a private repository for its + fixture ([#186](https://github.com/redhat-et/ripwire/pull/186)). +- **`pagingsweepcheck`** compared two cold `--whereis` runs that read the repository's shared ref namespace, so a + branch created by anything else between the runs failed the gate. Those arms now run on the gate's own fixture + ([#206](https://github.com/redhat-et/ripwire/pull/206)). +- **Three `--listen` gates share one HTTP client**, `test/lib/gatehttp.sh`. Readiness is an answered request, every + request has a deadline, and a missing answer is its own FAIL rather than a verdict about the server. A server still + warming up on a loaded macOS runner had read as "transports DIFFER" + ([#188](https://github.com/redhat-et/ripwire/pull/188)). +- **The public-tree check reads decks, not just text.** A private pre-release name had reached public files — prompt + text, an error message, a grader regex, docstring examples, `docs/EVALS.md`, three `src/` comments and six gates — + and was fixed forward, with history left alone. `ripwirepubliccheck` arm 1b now stores only the SHA-256 and the length + of the lowercase token, scans every tracked text file and every tracked deck (a deck it cannot read FAILS the arm), + and prints `path:line` only, so a red run's log does not republish what it is looking for. Other command names for + the agent-loop instrument come from `AGENTLOOP_TOOL_ALIASES`; the grade header and the grader's audit summary state how many + aliases are in force, never the names. The `release` CI job installs `pdftotext` for the deck extractor. Red first: + run against the merge base's checkout the arm fails with 36 locations in 13 files + ([#209](https://github.com/redhat-et/ripwire/pull/209)). + +### Changed — `mcpremotecheck` moves to the shared HTTP client + +The last `--listen` gate that still had its own HTTP client gets the same deadlines and no-answer FAILs. Against a +listener that stalled, it had hung. Against one that died, it had judged the silence as verdicts +([#194](https://github.com/redhat-et/ripwire/pull/194)). + +### Changed — aliasing contracts, checked in debug and read by the optimizer in release + +**`VERIFY_NO_ALIAS` is an optimizer fact in release, not an inert assume.** `src/infra/Diagnostics.h` §6's macro now +expands to `__builtin_assume_separate_storage` under `NDEBUG` on clang 17 and later (`__has_builtin`-guarded, +`( (void)0 )` elsewhere), beside the debug check, so codegen matches `__restrict__` on the parameters: the gate's +`out=a; out+=b; out+=a;` arm goes 10 → 6 instructions on arm64. The previous +`__builtin_assume( &a != &b )` form was never consumed by alias analysis, so it was a debug check that promised an +optimization it did not deliver. The promise is scoped honestly to the shipped binaries: the macOS x64 release binary +consumes it fully, the macOS arm64 binary — AppleClang 16, which is LLVM 17 — consumes it for scalar accesses only, +because BasicAA reads the bundle there only with the `-mllvm -basic-aa-separate-storage` flag CMake now probes for and +passes — to our targets and to the ld64 link under LTO — that switch being `cl::init(false)` in LLVM 17 (AppleClang 16 +/ Xcode 16.2: the macos-14 CI runners and the macos-arm64 release leg) and true from LLVM 18, and even then LLVM 17 +does not carry it into loop vectorization (fixed upstream in LLVM 18); the Linux release binaries, built with GCC, +keep the debug check alone. A new `VERIFY_NO_ALIAS_BUF` is the form for two owning +containers, where the promise has to land on the buffer rather than the object; views that can share one allocation are +refused at compile time. `test/noaliascheck.sh`, eight arms red against the old definition, compiles the real slice three ways with a `=false` +negative control and a cross-check against the cached CMake probe, and WARNs, naming the compiler and the upstream issue, where the loop +path is not consumed ([#200](https://github.com/redhat-et/ripwire/pull/200)). + +**Twenty-one functions state the contract at entry.** Fifteen functions whose two-or-more same-element-type +out-parameters would silently mis-compute or invalidate an iterator if a caller passed the same object twice now say so +at entry and abort on it in debug builds ([#201](https://github.com/redhat-et/ripwire/pull/201)); the last six — +`splitNoteTail` (`src/notes.h`), `takeAckNamedToken` and `computeDelta` (`src/quality.h`), `waterFillRecallShares` +(`src/recall.h`), `markCandidateFilesIncludingDecl` (`src/graph.h`) and `partitionByScope` (`src/verbs_quality.h`) — +complete the audit's apply list ([#211](https://github.com/redhat-et/ripwire/pull/211)), `computeDelta` with a +null-safe `VERIFY_TEXT` rather than the object form, because both of its out-pointers default to null. These are correctness contracts, not a +performance claim: for the object form the promise measured no codegen change, because it says nothing about a +container's heap buffer. One function is the tree's only codegen row — `waterFillRecallShares` in `src/recall.h` reads +`demand[i]` while writing `alloc[i]` and never resizes either, so it takes the buffer form: release codegen **309 → 301 +instructions** under the build's own flags. + +**House rule, with a finding behind it.** `CONTRIBUTING.md` now requires the `__restrict__` spelling. On macOS, +`` defines `__restrict` to nothing in every C++ translation unit, because `__STDC_VERSION__` is undefined +there, so any `__restrict` after a libc include was silently a no-op. Ripwire had none in `src/`, so this is a rule +rather than a fix ([#199](https://github.com/redhat-et/ripwire/pull/199)). + +### Fixed — a C++ header selector answered with definitions it could not tie to that header (`unproven_defs=`) + +A `file:name` selector that names only declarations is widened to the definitions they stand for. The candidate test +compared the name and `Symbol::scope`, the immediately enclosing class or namespace with namespaces dropped. So `a::Store` +and `b::Store` compared equal, and for a free function the test was the name alone. `--callers=a/Store.h:putObject` +answered `count="1"` for a caller in `b/Store.cpp`. `--callers=api.h:helper` counted two callers of different +internal-linkage `helper`s, in files that never include `api.h`. The true count for both is zero, and both answers +carried `counts_floor="1"`, a floor above the truth. + +A candidate definition is now kept only when its file is a declaration file or includes one. The include is resolved +path-precisely, never by basename, and a definition the proof cannot tie to the header is not widened to +([#173](https://github.com/redhat-et/ripwire/pull/173)). What the proof drops is counted, not left silent. `--callers` and +`--callees` carry `unproven_defs=` (#173). So do `--impact`, `--safe-delete` and `--path`, and MCP `impact` and +`path_between`. Those verbs had answered from the declaration alone, which has no call edges: `--safe-delete=api.h:helper` +printed `risk="none-found"`. Its legend now says that `risk="none-found"` beside `unproven_defs=` is an incomplete read, +never a sign that the name can go ([#190](https://github.com/redhat-et/ripwire/pull/190)). + +On ripwire's own tree, over every `file:name` selector whose selection is all declarations, 89 of 4,322 answers shrank +between the binaries before and after #173, and none grew. Two losses are known, and `unproven_defs=` counts both. A +`.cu`/`.cuh` include does not resolve for this proof, so a CUDA header selector can under-count. A body kept in a section +file that is pasted into a translation unit without including the declaring header is no longer reached; ripwire's own +`src/ingest.h:astQuery` is one. The one over-retention this left — an internal-linkage definition kept for another file's declaration of the same +name — is fixed below (#216). Gate: `test/decltodefcheck.sh`. + +### Fixed — the remaining silent zeros from a declaration selector + +`--uses`, `--mentions`, `--verify` and `--affected` answered a header selector from the declaration alone when the proof +dropped its definitions: `--uses` printed `count="0"`, `--mentions` `docs="0"`, `--affected` `tests="0"`, and +`--verify`'s `uses()`, `unused()`, `calls()` and `reaches()` answered from one declaration. Each now carries +`unproven_defs=` with a clause worded for that verb; `--verify` keeps its three verdicts and says that `not-established` +beside `unproven_defs=` is an incomplete read ([#195](https://github.com/redhat-et/ripwire/pull/195)). + +### Fixed — every verb that resolves a focus now says what it could not prove, and a declaration yields to its definition + +#173 stopped a `file:name` selector from following a declaration to a definition it could not prove belongs to it, and +#190 and #195 disclosed that drop on the graph and listing verbs. The verbs that resolve a *focus* symbol were still +silent. `resolveFocus` now returns the count, and the answer's root carries `unproven_defs="K"` — absent at zero — with +a per-verb clause: on `--edit-check` (including `--dry-run` and the MCP twin with and without `new_body`) it sits beside +`incompatible=` and says that an `incompatible="0"` next to it is an incomplete read, not a sign the edit is safe; on +`--lego`, `--connect` (summed over its terminals), `--around`, `--slice` and their MCP twins; on the `` root +`--expand` and `--outline` share, summed in every serving mode and charged in `est_tokens`; beside `--owners`' `defs=`; +as a JSON key on MCP `fetch_body`; and as its own stderr line on `--note-add`. + +A C/C++ declaration without a body also now yields the focus to the lowest-id C/C++ definition with a body in the same +scope. The rule is scoped on purpose: measured over all 12,996 names in this repository, an unscoped "prefer a body" +moved 132 picks, 69 of them wrongly (Python and JSON keys, TypeScript overloads, jumps between languages), while the +scoped rule moves 54, each a C/C++ declaration to its own definition. Four legend sentences that called the pick "the +lowest-id one" are reworded. `test/decltodefcheck.sh` grew 36 rows red on the merge base for the disclosure, 18 more for +the narrower sites and 9 for the focus pick; 212 of 212 pass now, also under ASan +([#210](https://github.com/redhat-et/ripwire/pull/210)). + +### Fixed — the callers answer's `next=` pointed at a `--uses` call that could not list the declined site + +0.6.0 gave `--callers` a `declined_calls=` count and a `next=` pointer to the `--uses` call that shows those call sites. +On a bare name the pointer landed. On a narrowed selector (`file:name`, `@FILE:LINE`, a canonical id or `Scope::name`) it +repeated the narrowed selector. That `--uses` answer keeps only sites that resolve to the chosen definition, and a +declined call resolves to none, so a reader who followed the pointer got `count="0"`. + +When a narrowed selector has declined calls, `next=` now names the bare-name `--uses` call in the XML, columnar and MCP +`find_referencing_symbols` answers. The callers legend says that list includes sites bound to other same-named +definitions. Every other answer keeps its bytes. `test/declinecheck.sh` arm (E) follows the pointer and requires the +declined site to appear, across Java, C++, Python and Rust spellings. + +Contributed by **@antoleod**, in their first contribution to ripwire +([#182](https://github.com/redhat-et/ripwire/pull/182)), closing [#158](https://github.com/redhat-et/ripwire/issues/158). + +### Fixed — a `#if 0` block stopped serving calls in 0.6.0 and went on serving every other role + +0.6.0 stopped serving CALL sites from preprocessor-dead ranges and left every other role serving them. A `role="write"` +inside `#if 0` is a write that cannot compile, and `--uses` counted it: on the gate's fixture `--uses=Owner.field` +answered `count="4"` carrying `counts_floor="1"` where the truth is 2 — a floor above the truth, which is the contract's +own failure direction. Reads, writes, both emitters of `role="import"` (`using ns::x;` and `#include`, the second living +in a code path #62 never touched), `extends`, types, the `#else` of `#if 1` and of `#if 0`, variable-to-type bindings and +dead definitions are all excluded now. The question is asked once per file and answered once per record, keyed on the +record's own site byte, rather than as five more `continue`s. + +Excluding dead *definitions* was the invasive half, so it was measured: on llvm-project (8,861 C-family files, 381,811 +symbols) it is **−14 symbols, +3 edges, −16 declined**, with `ambiguous`, `unresolved`, `est_tokens`, +`extent_suspect_syms` and the unindexed roll-up all unchanged; every dropped row is a real `#if 0` definition, among them +five in `Descriptor.cpp` that LLVM itself comments as not needed, whose names had been minting `overloads="2"` against a +live macro. Ripwire's own map is byte-identical to the base, and user CPU on llvm is 59.1 s against 59.4 s, interleaved. +The honest costs: `--grep` is unchanged (text inside `#if 0` is still findable, hit rows byte-identical), and +`--expand=deadType` now refuses with a suggestion rather than serving a dead body. A residual is disclosed rather than +left to be found: the FFI `BindingAlias` records carry no site byte, so an `extern "C"` block inside `#if 0` still +contributes its aliases; giving them one is a record-shape change this defect does not earn. `kParserVer` 93 → 94 with +its mirror; `kCacheVersion` stays 21, because no record gains or loses a field — only which records are extracted. Gate: +`test/ppdeadrolescheck.sh`, written and run red against the unmodified base binary first +([#172](https://github.com/redhat-et/ripwire/pull/172)). + +### Fixed — attributes an answer printed with no definition (`graph_unindexed=`, `--legend=compact`) + +`graph_unindexed=` counts the files no grammar in this build can read. It shipped in 0.6.0 on roots whose legend never +defined it: `--lego` on the CLI and over MCP, `--verify` and `--nonlocal-state`, whose legends are fixed text rather +than the shared builders. `--legend=compact` rebuilds its definitions from a table of terms, and that table had no row +for it, so it was also undefined under compact on every XML verb that can carry it except `--connect`. Both now define it +([#169](https://github.com/redhat-et/ripwire/pull/169)). + +The same table had no row for `declined_calls=`, `unproven_defs=`, `bodyless_defs=`, the `--uses=Owner.field` member +form, the multi-root `` table, `--lego`'s `methods="0" caveat=`, or `pr_iters=` on every PageRank root. +It also lacked the map-header fields whose `hdr:` definitions compact strips, among them `declined=`, `external=`, +`max_tokens=` and `over_ceiling=`, plus `--around`'s `defs=` and `--rank-by`'s `rank_by=` and `window=`. Each now has a +term that prints only when its attribute is present. Answers that carry these attributes can exceed the dialect's nominal +400 B; the alternative was a number with no definition ([#185](https://github.com/redhat-et/ripwire/pull/185)). + +### Fixed — twelve more attributes get compact definitions + +Under `--legend=compact`, these attributes now carry definitions: `--tree`'s `files=`; `--zoom`'s `symbols=`, +`isolated=`, `top_modules=` and `levels_shown=`; a cut ``'s `children=`; churn-decay's `` and +``; and the map rows' `lpin=`, `overloads=` and `prov=` ([#189](https://github.com/redhat-et/ripwire/pull/189)). + +### Fixed — compact definitions that did not fit the byte pins, so the pins now follow the definitions + +Several answers still printed attributes their compact legend never defined: the map header's own counts, `--impact`'s +blast-radius counts, `--safe-delete`'s verdict fields, the `--communities` and `--community` structure counts, and +`tested="1"` rows on `--callers`, `--callees`, `--impact` and MCP `impact`. Defining them honestly did not fit the +dialect's single 400 B per-answer pin, so the pins follow the definitions: each compact schema is pinned at its measured +size rounded up to the next 10 B plus 10 B — map 810 B (measured 799), communities 820 B (807), map-diff 800 B (789), +impact 780 B (770), community 730 B (719), safe-delete 720 B (708), around 720 B (707), metrics 720 B (702), +pack-signatures 680 B (663), pack-top-n 660 B (649) and query 630 B (611), with the remaining schemas between 140 B and +410 B. The ten-verb loop total goes from 4,100 B to 4,900 B (measured 4,849 B), and MCP `impact` is pinned at 780 B. + +A read-only review of every definition against the code that emits it found three readings that were wrong, all +corrected here: `--safe-delete`'s `t=`/`p=` name the lowest-id *match*, not the lowest-id definition, because a +header-qualified selector keeps its declarations; `changed=` counts only indexed git-changed files and is 0 when git +cannot be read; and `--communities`' `bridges=` counts community pairs, one-symbol communities included. + +`--help` no longer claims a fixed "≤400 B legend": it states the per-verb sizes and, restated from measurement, a saving +of "at least 45%" on a small `--callers`/`--uses`/`--impact`/`--affected` answer, down from "at least 50%" — the measured +savings are 65.91%, 63.79%, 46.17% and 64.73%, a per-call drop of 2.8–5.8 KB. Byte identity was checked against the +pre-change binary: 39 of 40 non-compact answers are identical (`--help=all` is the only difference), and all 26 compact +answers keep every row and data comment byte-identical, with only legend text changing. Left for a follow-up: +`tested="1"` is still undefined under compact on `--pack-task`'s `` body rows and in the columnar `tested` column +([#203](https://github.com/redhat-et/ripwire/pull/203)). + +### Fixed — `--for`'s `over_ceiling=` verdict could be written by the task text, and rung zero dropped legend clauses without a word + +`--for` found which ceiling-ladder rung had fired by searching the finished header for that rung's note, and the header +also carries the task echo verbatim. A task containing that note got `over_ceiling="1"` on a document well inside its +budget. Since 0.6.0 the same search also ran inside the fit predicate, where matching text could push a real bundle down +the ladder. The ladder now returns the rung it took, so no task text reaches the verdict. + +Rung zero, which drops legend clauses to fit, dropped the definitions of `confidence=`, `margin_pct=` and +`budget_tokens=` while keeping the attributes. It now names each attribute whose definition it drops, in the same shape +as the rungs above it. `test/ceilingverdictcheck.sh` is new, and `test/legendcoveragecheck.sh` gains a budgeted `--for` +row ([#174](https://github.com/redhat-et/ripwire/pull/174)). + +### Fixed — `--connect`'s `est_tokens=` left out a legend clause it printed + +When a file in the tree was unindexed, `--connect` added the `graph_unindexed=` attribute and a legend comment defining +it, but charged only the attribute to its estimate. `est_tokens=`, the `--max-tokens` fit and `over_ceiling=` therefore +measured a smaller document than the one emitted. The v0.6.0 binary was run on a matched pair of corpora, identical except +for one unreadable file: the document grew by 205 B while `est_tokens=` stayed at 1,068. The fixed binary reads 1,142. +The clause is now one named string that both the charge and the write read +([#171](https://github.com/redhat-et/ripwire/pull/171)). + +In the same change, `skills/install.sh --hermes` links only the `ripwire-*` directories under `skills/hermes/`, as its +other two loops already did. The only directory there today is `ripwire-repo-map`, so no install changes. + +### Fixed — `--help` left out twelve flag rows, and `--help=--FLAG` said they did not exist + +`--help`'s one-line tier treats a row indented four spaces as a flag and anything else as prose. Twelve rows in +`src/cli.h` were indented six, so the flags on them were culled from `--help`, among them `--and`, `--not`, `--scope`, +`--partition`, `--dry-run`, `--apply` and `--grep-context`. `--help=--and` answered that it matched no flag, while +telling the reader that `--help` lists every row. The flags themselves always worked. + +The rows now sit at four spaces, and the `docs/COMMANDS.md` generator accepts exactly what the binary accepts. +`test/helpbudgetcheck.sh` arm (K) takes its population from the flags `parseArgs` accepts, so a new flag missing from +`--help` turns it red. The eleven flags still unadvertised are listed in the gate, each with a reason +([#170](https://github.com/redhat-et/ripwire/pull/170)). + +### Fixed — a client that dropped a large reply killed the `--listen` server + +`ripwire --listen` wrote replies with a plain `send()`, and nothing handled SIGPIPE. A client that closed its connection +before reading a reply larger than the socket send buffer raised SIGPIPE, which ended the server, and every later client +was refused. Each socket now suppresses the signal, with `MSG_NOSIGNAL` on Linux and `SO_NOSIGPIPE` on macOS, so the +failed send drops that one connection and the server keeps serving. The CLI's stdout behaviour is unchanged. +`test/mcpremotecheck.sh` drops a client three ways against a reply larger than 4 MiB, and requires the same listener to +answer the next request ([#187](https://github.com/redhat-et/ripwire/pull/187)). + +### Fixed — `--scip` ignored every index scip-java writes + +ripwire read only SCIP's deprecated `Occurrence.range` field. scip-java writes the `typed_range` form instead +(`single_line_range` / `multi_line_range`), so no occurrence ever joined, and `--scip` produced output byte-identical to a run +without it. The reader now takes the typed form, and it outranks a deprecated `range` on the same occurrence whichever +arrives first, as `scip.proto` asks. On spring-petclinic the overlay went from no matches to 79% of occurrences, and +`graph_ambiguous` from 6 to 0. `test/scipcheck.sh` arm 10 re-encodes its fixture in the typed form (red on the old +reader), and arm 10b proves the fixture cannot pass without the typed fields. Contributed by **@dpunosevac**, in their +first contribution to ripwire ([#198](https://github.com/redhat-et/ripwire/pull/198)). + +### Fixed — three surfaces said a missing `--scip` index degrades; it refuses + +Since v0.4.0, a `--scip` path that cannot be opened exits 1 and serves no map, while a corrupt index still warns on +stderr and proceeds name-based. The `--scip` help row, the README and `skills/ripwire-navigate/SKILL.md` said a missing +index degrades and never fails. They now say what the binary does +([#184](https://github.com/redhat-et/ripwire/pull/184)). + +### Fixed — `--scip` refuses a path that is not a regular index file + +A `--scip` path that is empty, a directory, a FIFO or a device now exits 1, as a missing one does, instead of serving +the name-based map at exit 0. A FIFO had hung the run. The index is opened again when it is loaded, and that open no +longer blocks either: a path replaced after the check by something that is not a regular file degrades with the usual +warning ([#197](https://github.com/redhat-et/ripwire/pull/197)). + +### Fixed — a symlink at a sidecar name is refused, not written through + +`.ripwire_notes`, `.ripwire_quality_baseline` and `.ripwire_arch_baseline` are opened for writing with `O_NOFOLLOW`. When +one of those names is a symbolic link, the link is not followed: the write exits 1 with a message on stderr, and the link +is left in place. A sidecar that is a regular file is written as before. The arch baseline writer now also reports a +failed write, where before it could report success ([#178](https://github.com/redhat-et/ripwire/pull/178)). + +### Fixed — a symlink at a sidecar name is not read through + +The readers of the same three sidecars open them with `O_NOFOLLOW` too, so a symlink at a sidecar name is refused on +read as well as on write. Anything at a sidecar name that is not a regular file, a FIFO for example, is refused +instead of waited on, and a sidecar is emptied for rewriting only after it has been confirmed to be a regular file +([#191](https://github.com/redhat-et/ripwire/pull/191)). + +### Fixed — the crawl does not follow a symlink out of its root + +A symlink inside the crawl root whose target resolves outside that root is not followed. Every walk that reads files +skips it and lists it on `--skipped` in a new `escaped` class. It is counted as `escaped_root=` on the map header (XML and +JSON), `` and ``. The attribute is absent at zero, so a tree without such a link gives byte-identical +output, and a symlink that stays inside its root is indexed as before +([#179](https://github.com/redhat-et/ripwire/pull/179)). + +### Fixed — the suite's `skip=` count stopped depending on where the checkout lives + +`test/pargates.py` decided whether a gate had SKIPPED — ran, but proved nothing — from the word SKIP in the first +400 CHARACTERS of its transcript, and 515 of the 628 transcripts of one full run open with a banner naming the +checkout's own absolute paths. The same commit and binary reported `skip=2` from a 137-character checkout and +`skip=3` from a 38-character one; 24 gates print a skip marker downstream of an absolute-root mention, the nearest a +real standing skip declared 145 characters in. The rule is written down instead of measured in bytes: **a gate that +proves nothing says so before it claims anything** — the first verdict marker decides, and a SKIP after a PASS or +FAIL is an arm-level skip inside a gate that did prove something. Replayed over those 628 transcripts, the new rule +and the old one disagree on ZERO gates. One direction is newly open and disclosed rather than left to be found: a +whole-gate skip printing a PASS row above its skip marker would read as a pass, which no gate does today and nothing +yet enforces. Gate: `test/skipclassifycheck.sh`, driving the real harness over one byte-identical probe from two +corpus roots about 130 characters apart, with `test/gateexitcheck.sh` arm (D) as the gate side of the contract +([#223](https://github.com/redhat-et/ripwire/pull/223)). + +### Fixed — a relative command with no anchor, and a cap that bounded the answer and not the work + +Fifteen defects from four review rounds over the three `--situ` entries above. **A `run=` is a command, and its path +comes from the corpus:** the runner verb and the path were concatenated, so an unusual but legal filename could +produce a `run=` that does not parse as the single command it presents itself as. The path is now always one shell +argument — quoted whenever it is not provably safe, by an allowlist that quotes any unenumerated byte, and preceded +by an option terminator so no path reaches an interpreter as an option. Every tracked path here is inside the +allowlist, measured at 0 outside it, so the emitted bytes are unchanged on every real corpus and no pin moved; +`test/runhintcheck.sh` arms (5) and (6) EXECUTE the emitted command in a scratch corpus, each with the pre-fix +spelling as its control. **A relative command is only as good as its anchor:** four surfaces spelled a path or a +command relative to a root they never declared — the shared run-hint clause on a multi-root run, `--flags --flip`, +the MCP edit receipt, and `--help` — and the one relativizer every `p=`/`uri=` emitter routes through returned early +on a leading `./` and matched a prefix only when the next byte was `/`, which the filesystem root can never satisfy. +`test/rootrelemitcheck.sh` arm 9b prints one command for six root spellings and executes each; `test/sarifcheck.sh` +arm 11 drives the function over 22 rows, 4 red before. **A cap bounded the answer and not the work:** the new +lexical-siblings block compared every unchanged indexed file against every changed path with the row cap applied +only after collection, O( (F + U) × C ). Changed paths are indexed by directory once into a sorted vector searched +with `lower_bound` (no `std::map`), the predicate still called on the narrowed range. Interleaved, best of 5, `-O2` +with the shipped flags, on a host at load 38 on 18 cores — so the absolutes are upper bounds and the ratio is the +measurement — llvm-project `4d5358b1` (8,856 paths, C=2,000) reads 333 ms → 11.6 ms and golang/go (12,555 paths) +449 ms → 42.8 ms, its second pass 1,005 ms → 136.5 ms; that llvm population grown to `docs/EVALS.md`'s 182,555-file +rung, synthetic in SIZE only, reads 9.10 s → 22.6 ms. Emitted rows are byte-identical on all nine rungs (1,413 +rows). The same block paged on another section's offset and went silent on an empty candidate list — a silent zero, +which `docs/METHODOLOGY.md` §9 forbids — and four disclosures compressed into attributes kept a short reading each, +since `--situ` has no legend to look a name up in. Measured with `wc -c` against this lane's base binary: +`--situ=src/graph.h` 4,448 → 2,955 B and `--test-gate=src/testmap.h` 5,455 → 5,247 B. Gates: +`test/situshapecheck.sh` (17 rows red on that base binary), `rootrelemitcheck`, `runhintcheck`, +`test/receiptpostcheck.sh` (18). The lane's own sibling-row cap makes the cap inventory 211, republished by its +generators rather than edited ([#219](https://github.com/redhat-et/ripwire/pull/219)). + +### Fixed — two generated documents, a scoped run's ranking notice, and a tilde no shell expands + +Three from the scoped-recency lane's own review. **`docs/COMMANDS.md`'s generated table of contents minted anchors +by substituting a hyphen for every run of non-alphanumeric characters where the renderer DELETES that punctuation**, +so all 169 links resolved to nothing and had done since the document was first generated; markdownlint's MD051 had +been reporting it 28 times on one line. The generator states the renderer's own rule now and assigns anchors in +emission order, audited by `test/docscommandscheck.sh` arm (J), which restates that rule rather than importing the +generator's — a gate that asks the generator what an anchor should be agrees with its mistake. `docs/TUNING.md`, +likewise generated, asserted a sum instead of deriving one ("`112 + 12` accounts for the 128 NAMES" is 124) in the +one paragraph whose subject is that quoting a wrong pair would be wrong in both halves at once; recounted from the +data its table is built from, `src/` declares 129 caps under 128 distinct names, 111 tunable, 12 that must stay +`constexpr` and 5 declared after the sweep was frozen, and `capsweep.py emit` now REFUSES to render a partition that +does not add up. **A scoped run said its ranking fell back, having run no ranking:** under `--in=DIR` the +uniform-prior notice was false three ways at once — nothing is ranked on a scoped run (the rank vector is +zero-filled, which is why the header carries no `pr_iters=`), "this map" named a document the run does not contain +since the map IS the stub, and the comparison it offered is refused beside `--in`. **And a pasteable `next=` quoted +a tilde no shell expands:** `nextFlag` quoted any value whose first character is `~` whether or not a flag name +preceded it, so a run under `--exclude=~tmp` published `--exclude='~tmp'` against an expansion that cannot +happen — the guard is "word-initial AND no flag name" now, a narrowing rather than a deletion. The worse half was +the gate, which pinned the corrupted form and explained it with a belief about POSIX that is wrong twice over; a gate +that pins a false belief defends the bug against the next person to fix it, so the explanation is deleted rather +than reworded and the measured rule stated in its place. Gates: `test/recentscopecheck.sh` 13e/13f, +`test/capsweepcheck.sh` (C), `test/nextverbcheck.sh` (9) +([#212](https://github.com/redhat-et/ripwire/pull/212)). + +### Fixed — an unmeasured `est_tokens` said nothing, a no-throw contract threw, and two test-row readers went quiet + +Six defects from one review, each a surface that was silently wrong rather than loudly broken. **`--pr-context` +shipped a modelled `est_tokens` with no disclosure:** when a trim level's measurement render fails it returns an +empty body, the ladder priced that empty body and the root printed the price, while the verb correctly streamed the +untrimmed floor. The only signal was a `DEGRADED_PATH_ALERT`, which is `do {} while (0)` under `NDEBUG`, so the +binary a user installs published a modelled number with nothing saying so (non-negotiable #3). The bytes were never +the bug and are unchanged: `truncated=` carries `;est-unmeasured`, defined in the legend in the same voice as +`budget-floor-exceeded`, which takes the tail's worst case from 248 B to 263 B and its buffer from `tail[256]` — +seven bytes of margin, as `test/fixedbufsweep.sh` had warned in terms — to `tail[320]`. **`renderToString`'s +no-throw contract had a throwing last statement:** the one allocation on the success path sat outside the handler, so +a `std::bad_alloc` escaped a function documented to return `ok == false` and jumped the `free()` below it. It is +caught in its own handler now and the buffer is released exactly once on every path, proved by a fault switch in +`test/prcontextcheck.sh` arm (G), red on the parent commit and honest in both build flavours. **The shared test-row +reader scanned arbitrarily far forward for a `[`**, so a `null` field's answer came out of the NEXT field's array at +exit 0 where its docstring promised a parse error; the value is read adjacently now. And two `test/` path readers +had never been converted to that reader — one splitting every row on `,`, one matching single rows only and +returning the empty set on a two-row fixture ([#214](https://github.com/redhat-et/ripwire/pull/214)). + +### Fixed — a ceiling priced in the wrong unit, two price lists for one comparison, and shapes that outlived their output + +Fifteen findings from two review rounds of the short-id and compact-legend lane. **The ceiling was priced in the +wrong unit, and then stopped being a byte test at all.** `--for` and `--pack-task` tested their ceiling rungs at +`kMinBytesPerToken` (2.36) while `est_tokens=` prices the delivered document per kind — markup at 2.50, bodies at +3.80 — so a lens spent rungs against a ceiling it was not measured against and dropped legend definitions from +documents its own root reports as conformant. Measured on a five-symbol fixture at `--detail=1`: at every budget in +1069..1099 the kept document prices at `est_tokens="1069"` with no `over_ceiling=`, and the rung dropped all three +clauses anyway to deliver 715 — 354 tokens of headroom spent to buy nothing. The exact ceiling is the token +comparison itself now, asked once on the finished document; the allowance rungs stay byte-based, which is their +contract, and no tolerance was widened. Separately the 1.15 overshoot tolerance had gated the first free drop as +well, so a document 1–15% over budget shipped `over_ceiling="1"` with all three explanatory clauses riding; that +drop is now tried against the number the root promises. **Rung zero also stopped being byte-negative:** it removed +110–164 bytes of clauses and spliced a 161-byte note naming them, +51 bytes on a route-less compact answer, so the +candidate is built and compared and a drop that does not pay is not taken. **`--expand` chose its serving mode on +two different price lists:** the whole-file candidate was charged its raw bytes and its legend — no envelope, no root +attributes, no closing tag — so on a fixture whose symbol sits in an 864 B file the root said `reason="file 1100B +< bundle 1193B"` over a document that came out 1,262 B, selecting and reporting the whole-file form while the +bundle it rejected was smaller. Both candidates are one document type now, priced by one function that charges the +whole served document and settles the self-referential `mode=`/`reason=` disclosure with the same ≤4-pass fixpoint +`est_tokens=` uses; `expandmodecheck` (4a)–(4d) assert that `reason=`'s own count equals `wc -c` of the delivered +document in all three modes and sweep seven paddings across the decision boundary. **Three surfaces answered in +shapes the tool no longer produces:** the MCP file page's hand-composed `route=`, `--expand`'s whole-file serving +printing a full canonical id inside a `` that had just printed the path, and a benchmark keyed on the retired +`id=` that collapsed two same-named methods into one key. A merged callee row was charged `name+16` while printing +about four bytes, so a block of overloads wrote `capped="1"` over a listing that would have fit, and `l=` is sorted +rather than appended in rank order. The route hooks' command-word rule asked the shell to split a line, which never +separates a control operator from the word attached to it, so `true; ripwire .` read as not-a-call; it lexes the line +itself now, quote-aware, executing nothing. Four gates were enforcing or reporting the wrong thing — one matching map +rows by the retired spelling, checking zero rows and printing PASS; one holding a hand-typed verb list that made it +enforce a command the binary refuses; one counting two of three droppable clauses; one discarding every exit status +under `eval … || true` — and every compact-legend pin was re-derived from its own stated rule, which seven rows had +not been following. And the compact-legend policy was audited in the direction that matters for the first time: both +existing arms started from a command that already carried the flag, so a command that should carry it and does not +was invisible to the gate; seven spans across six skills are fixed, and the gate asks the binary both halves of the +question now. `docs/LINEAGE.md`'s unqualified "no network" names its one documented exception in the same round: a +git URL is shallow-cloned before it is mapped ([#215](https://github.com/redhat-et/ripwire/pull/215)). + +### Fixed — a header's declaration no longer widens to an internal-linkage definition (parser version 96) + +The decl-to-def widening behind every `file:name` selector (`--callers=api.h:helper`, `--impact`, +`--uses`, `--safe-delete`, the MCP twins) kept a same-named definition when its file `#include`d the +declaring header. That proof is per FILE, and a translation unit that includes `api.h` for its own +reasons may define an unrelated `helper` in an anonymous namespace or as a namespace-scope `static` — +an overload (`helper(double)` beside the declared `helper(int)`) compiles, and by name it was gathered +and served. Internal linkage makes a definition visible to its own translation unit alone, so no other +file's declaration can stand for it. Raised by CodeRabbit on #139 after merge, outside the diff. + +Every C and C++ definition now carries a syntactic `internalLinkage` bit — inside an anonymous +`namespace { }` at any depth, or carrying a namespace-scope `static` (a class-scope `static` member has +external linkage and is not marked). The widening keeps such a definition only for a declaration in its +own file and otherwise counts it in `unproven_defs=`, so the reader still learns that same-named +definitions exist which no row covers; the bare-name selector still shows them, as the legend says. + +Measured on the gate fixture (`test/decltodefcheck.sh` arm B2: a header, its defining `.cpp`, one real +caller, and two including TUs with an anonymous-namespace and a `static` overload): `api.h:helper` +answered `count="3"` on main where `api.cpp:helper` answered `count="1"`; it now answers `count="1"` +naming the one real caller, with `unproven_defs="2"`. On this repository at `1cf3086e` the default map +is byte-identical and none of the 11 header-qualified `--callers` selectors over `src/ingest.h`'s +declarations moved (the tree has no colliding internal-linkage overload). `kParserVer` 95 → 96 and +`kCacheVersion` 21 → 22 (the def record gains one byte) with `quality.h`'s mirrors in the same commit; +old caches are rejected and rebuilt +(@andriytyurnikov, [#216](https://github.com/redhat-et/ripwire/pull/216)). + +### Planned for 0.6.2 + +- **A redone "What `--quality-delta` catches" slide.** It left the showcase deck in 0.6.1, and returns with stronger + examples, each reproduced from a real repository. +- **Faster cold Elixir ingest.** Elixir parsing walks each node's ancestors to find its scope, and costs about three + times the CPU of other languages; one top-down pass that keeps a scope stack should recover it. +- **Calls brought in by Elixir's `use`.** 0.6.1 counts them as unresolved, so the drop is disclosed; 0.6.2 aims to + model `__using__` so they resolve. +- **Header selectors that know C++ namespaces.** `scope` drops the namespace chain, so a definition in a *different* + namespace with the same name is still kept for a header's declaration; 0.6.1 closed the internal-linkage half of + that over-retention (#216) and recording the chain itself is what closes the rest. +- **A full head-to-head, measured on a quiet machine.** Cold index time, per-query latency, peak memory + and answer size in tokens for equivalent questions, each axis pre-registered with its corpus, method, N and medians, + both versions named, and a reproducible harness published beside the result. Nothing from it is published until the + run is done on a machine that is not doing anything else. +- **The same comparison with an agent in the loop.** Several agent sessions per task across the frozen retrieval + questions, change-safety tasks, real merged PRs and a SWE-bench Verified subset, starting from a pilot sized against + the pre-registered instruments. Every loss is traced, fixed and re-measured before any result is published, wins and + losses both. +- **`--legend=compact` as the CLI default.** The agent surfaces ask for it in 0.6.1 and the bare CLI does not. Flipping + the default moves a published contract, so it happens under a pre-registered terminality readout — does the compact + answer still end the task in one call — rather than on a byte count. +- **`--for --in=DIR`.** `--in` scopes the churn-decay map in 0.6.1; the same scope belongs on the ranked bundle and on + its widening page. +- **A hunk-seeded `--situ`.** `--situ` reads changed files; seeding it from the diff's hunks would let the blast radius + start from the lines that moved rather than the files that contain them. +- **De-ranking test paths.** A ranked answer to a question about production code still spends rows on the tests that + exercise it; the ranker should know the difference and say when it has applied it. +- **`install.sh` served as a release asset.** The one-line install command will fetch the installer from the latest + release, next to its checksum, instead of from `main`. ## [0.6.0] — 2026-09-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index 46432c70..44cc9f12 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) # The version string lives in exactly ONE place — this project() call. --version (main.cpp) # reads it via the generated version.h below; test/versioncheck.sh asserts the two never drift. -project(ripwire VERSION 0.6.0 LANGUAGES C CXX) +project(ripwire VERSION 0.6.1 LANGUAGES C CXX) # ---- language standards (C++23 for our own code / C11 for the vendored C dependencies) ---- set(CMAKE_CXX_STANDARD 23) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2dac4a66..16debe3f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -289,6 +289,21 @@ already knew about the others, several while fixing one. So the rule is mechanic Guard, don't assert. - Throw only at the `operator new` seam. A throw escaping a worker thread is `std::terminate`, so wrap thread bodies in `try { … } catch( ... ) { … }`. +- **Avoid exception handling. Where a throw is unavoidable, RAII is what makes the code exception-safe: + cleanup belongs in a destructor, never in a `catch`.** A handler that releases a resource has to know + which resources are live at the point the throw happened, so such handlers multiply — two throw sites + in one function own different things and need different teardown, and the handler is only correct + until someone adds an early `return` above it. One owner whose destructor releases what it holds + collapses that to a single handler whose only job is the conversion this codebase actually wants: a + recoverable error becomes a degrade, returned, never propagated. Measured on `216802ad`, 2026-09-14: + of **27 `catch` blocks under `src/`, exactly one releases a resource by hand** — `infra/emit.h`'s + `renderToString`, which `fclose`s a memstream and `free`s its buffer. The other 26 convert a throw + into a degrade, set a flag, return a message, or `continue`; they own nothing, which is why they are + one line each. Re-derive rather than trust: a bare `grep -cE '\bcatch[[:space:]]*\('` over `src/` + reports **35**, and 8 of those hits are the word inside a `//` comment or inside a tree-sitter query + string — most of them in `lintrules.h`, whose subject is *detecting* empty catch blocks in other + people's code. Exclude comment and string context, then read each surviving handler's first body + line, because the resource question is answered by reading it and not by counting. ### Naming encodes what the type cannot diff --git a/README.md b/README.md index add6cedd..afc7ae75 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ format, exit codes and limits. You do not need it to get started.

Trendshift: C++ Repository of the Week badge for redhat-et/ripwire

-Fifty years of software-engineering results, and research from last month. 49 repositories and 70 papers folded — McCabe (1976) through to seven published in the last two months — each row in docs/LINEAGE.md naming the lesson taken and the file it lives in +Fifty years of software-engineering results, and research from last month. 49 repositories and 71 papers folded — McCabe (1976) through to seven published in the last two months — each row in docs/LINEAGE.md naming the lesson taken and the file it lives in Beside those sits a labelled survey of **237 tools** that contributed nothing and says so. The two sets are disjoint by construction, so they add rather than nest — a tool that gave a lesson is never @@ -53,9 +53,19 @@ claim cannot quietly drift. The row-by-row ledger is JavaScript · Java · Ruby · PHP · Lua · Elixir · Dart · Kotlin · Bash · C# · JSON · TOML · YAML · Markdown — see [language support and limits](#languages). +**ripwire 0.6.1 — out now. The answers an agent reads got smaller.** A compact answer is 46–66% smaller per +call — 2.8–5.8 KB less on `--callers`, `--uses`, `--impact` and `--affected` — the flagless map is 15.3% smaller at +identical rows, and the new `--in=DIR` scopes "what changed recently" to a directory: 39.8 KB down to 10.2 KB per +answer on RocksDB. It got smaller in memory too: on llvm-project the declined-call index drops from **114 MB to +368 KB**, with every count and every byte of output unchanged. Elixir resolves natively by module, name and arity +(thanks @henry-hz), `--scip` reads scip-java indexes (thanks @dpunosevac), and a `file:name` selector no longer +answers with a definition that belongs to another file (thanks @andriytyurnikov). Every number in a compact answer +now arrives with its definition, and the focus verbs say when a definition could not be proven instead of +reporting a quiet zero. + **ripwire 0.6.0 — out now.** Kotlin and Dart bring it to 24 vendored grammars, and Ruby now reads the dependencies a Rails application actually has: superclasses, mixins, `autoload`, and the constant receivers an autoloader loads -through. On llvm-project — 182,555 files — the cold parse drops from 194 s to 156 s. Declined calls, derailed parses +through. On llvm-project — 182,555 files — the cold parse drops from 194 s to 156 s of CPU. Declined calls, derailed parses and cut answers now say so, instead of returning a quiet zero. **[The presentation](present/ripwire-showcase.pdf) · [the changelog](CHANGELOG.md)** — with thanks to the @@ -798,7 +808,7 @@ Full retrieval tables — including the MRR figures behind the router numbers ab

- ▶ The whole tool in 33 slides — every figure names the instrument that pins it
+ ▶ The whole tool in 34 slides — every figure names the instrument that pins it
renders in your browser · pptx beside it · the numbers behind it

@@ -827,9 +837,10 @@ the recommendation — and abstains honestly when the evidence is too thin to na Which surface is the authority — --help vs docs/COMMANDS.md — and the four reflex verbs worth memorising `./build/ripwire --help` is generated from the binary's own flag table and is always the authority; -[`docs/COMMANDS.md`](docs/COMMANDS.md) documents every one of the 145 documented flags — 94 of them -with a real invocation and its recorded output (counts re-derived 2026-08-23; `test/docscommandscheck.sh` -fails if that documented set and the binary's own flag table ever disagree). Each family below links there. +[`docs/COMMANDS.md`](docs/COMMANDS.md) documents every one of the 176 documented flags — 162 of them +with a real invocation and its recorded output (both counts are reported by the generator that writes the +document, `docs/docs_commands_build.py`, and were re-derived from it on 2026-09-14; +`test/docscommandscheck.sh` fails if that documented set and the binary's own flag table ever disagree). Each family below links there. Four reflexes worth wiring into muscle memory: `--from-trace=FILE` for an error you have in hand, `--edit-check=SYM` right after an edit (did the contract change, and which callers are now provably @@ -1320,7 +1331,7 @@ cmake --build build 2>&1 | ./build/ripwire . --from-trace=- **0.6.0 — 2026-09-11.** Kotlin and Dart bring the vendored grammars to 24; Ruby now reads the dependencies a Rails application actually has — superclasses, mixins, `autoload`, and the constant receivers an autoloader -loads through. On llvm-project, 182,555 files, the cold parse drops from 194 s to 156 s. Declined calls, +loads through. On llvm-project, 182,555 files, the cold parse drops from 194 s to 156 s of CPU. Declined calls, derailed parses and cut answers say so now, instead of returning a quiet zero. Every release, with its measurements and its caveats, is in **[CHANGELOG.md](CHANGELOG.md)** — that file is the @@ -1648,10 +1659,10 @@ timing-only, and `pmccheck`'s inactive arm now proves that was truly the case. ## Standing on the whole field
-43 repositories, 69 papers and a 237-tool survey — and the study where search over a pre-built index beats a delegating planner 65.2% to 46.2%, at under half the cost +49 repositories, 71 papers and a 237-tool survey — and the study where search over a pre-built index beats a delegating planner 65.2% to 46.2%, at under half the cost Almost none of the ideas here are new; the combination and the constraints are. Lessons folded from -**49 repositories and 70 papers** into one deterministic executable, alongside a labelled +**49 repositories and 71 papers** into one deterministic executable, alongside a labelled survey of 237 tools that folded nothing and are catalogued separately — the two sets are disjoint, so they add rather than nest. The row-by-row ledger, each with the lesson taken and where it lives, is [`docs/LINEAGE.md`](docs/LINEAGE.md). Those three counts are derived from that document's own tables @@ -2042,14 +2053,14 @@ tier: it parses with its own vendored grammar, so its headings are symbols, not > there. Come here when you want to know exactly what a command does, what its output means, or where the tool stops > being right. -Publication date: 2026-09-12. Every claim below re-verified against a 0.6.0 build on 2026-09-13. +Re-verified against a 0.6.1 build on 2026-09-14. -ripwire analyzes a source tree. The tool writes a ranked symbol map to standard output. The map -shows the symbols that matter for a task, the callers of those symbols, and the tests that reach -them. The tool is one binary. The map itself has no runtime dependencies: no API key, no embeddings, -no index server, no daemon. The history-backed commands need `git` on the path and a repository -to read. It uses a network only when you give it a git URL instead of a directory, which -it shallow-clones into a cache. +ripwire reads a source tree and writes a ranked symbol map to standard output: the symbols that matter for a +task, their callers, and the tests that reach them. One binary, and a map that costs nothing to serve — no API +key, no embeddings, no index server. The history-backed commands need `git` on the path and a repository to +read. Two commands reach the network and nothing else does: a git URL in place of a directory, which is +shallow-cloned into a cache, and `--mcp --listen=HOST:PORT`, which binds a socket and is the one mode that +serves other processes (see `--mcp-token` and `--allow-remote-edits` before exposing it). This guide tells you how to install, operate, and evaluate ripwire. Read `docs/COMMANDS.md` for the full command reference. Run `ripwire --help` for the current flag list. The binary generates @@ -2057,9 +2068,9 @@ full command reference. Run `ripwire --help` for the current flag list. The bina `--help`, report this guide as a defect.
-Evidence basis. Fifty years of software-engineering results. 49 repositories and 70 papers are folded. A survey of 237 tools is separate. +Evidence basis. Fifty years of software-engineering results. 49 repositories and 71 papers are folded. A survey of 237 tools is separate. -The counts come from `docs/LINEAGE.md`. Counts are current as of 2026-09-08. Seventeen of the folded papers are from 2026, seven published in the last two months, and three in the last thirty days. This project folds 49 repositories and 70 papers into one executable. The survey describes 237 tools that contributed no lesson. The two sets are disjoint, so the counts add. +The counts come from `docs/LINEAGE.md`. Counts are current as of 2026-09-14. Seventeen of the folded papers are from 2026, seven published in the last two months, and three in the last thirty days. The survey describes 237 tools that contributed no lesson. The two sets are disjoint, so the counts add.
@@ -2770,7 +2781,7 @@ terms. | The compile-time cap inventory | [`docs/LIMITS.md`](docs/LIMITS.md) | | The measured cost of each cap | [`docs/TUNING.md`](docs/TUNING.md) | | Skill-file security checks | [`docs/COMMANDS.md`](docs/COMMANDS.md#--scan-skillsdir) | -| The whole tool in 33 slides — the showcase deck | [`present/ripwire-showcase.pdf`](present/ripwire-showcase.pdf) ([pptx](present/ripwire-showcase.pptx), rebuilt by [`present/deck5_ripwire_build.js`](present/deck5_ripwire_build.js)) | +| The whole tool in 34 slides — the showcase deck | [`present/ripwire-showcase.pdf`](present/ripwire-showcase.pdf) ([pptx](present/ripwire-showcase.pptx), rebuilt by [`present/deck5_ripwire_build.js`](present/deck5_ripwire_build.js)) | If a document disagrees with `--help`, the document is the bug. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 72a7d22d..608d7305 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,7 @@ 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-13.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-09-14.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. @@ -75,10 +75,10 @@ $ ./build/ripwire . --top-k=5 - - + + - + @@ -112,10 +112,10 @@ $ ./build/ripwire . --max-tokens=1500 - - + + - + ... [1 more line(s); run it to see the whole thing] @@ -141,7 +141,7 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--recall`, `--handoff`, `--pr-context`, `--from-trace`, `--run-trace` @@ -164,7 +164,10 @@ _The honest half of the contract: a task with no ripwire-shaped evidence ABSTAIN ``` $ ./build/ripwire . --help-task="write a cheerful release announcement" - + + + + ``` **Caveats (stated by the binary):** @@ -183,14 +186,14 @@ _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 -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -220,14 +223,14 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./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 -### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -252,16 +255,16 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./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 * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) +TSTree* parseTree( TSParser* parser, std::string_view src ) Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() - + errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth enum class AstWalk : std::uint8_t constexpr std::array<LangEntry, 48> kLangTable = @@ -287,18 +290,18 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./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 ) - +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… -struct RankedGraph +struct RankedGraph ... [19 more line(s); run it to see the whole thing] ``` @@ -320,16 +323,16 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./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 * 1024u -TSTree* parseTree( TSParser* parser, std::string_view src ) +TSTree* parseTree( TSParser* parser, std::string_view src ) Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() - + errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth enum class AstWalk : std::uint8_t constexpr std::array<LangEntry, 48> kLangTable = @@ -354,7 +357,7 @@ _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 - @@ -385,11 +388,11 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +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 ) @@ -417,7 +420,7 @@ $ ./build/ripwire . --lego=Vehicle - + @@ -446,9 +449,9 @@ _The repo's best-in-class instance to imitate before writing new code (picked by ``` $ ./build/ripwire . --exemplar="format byte sizes for humans" - + - + f, A&&... a ) { // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; @@ -476,20 +479,20 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ ``` $ ./build/ripwire . --recall="quality delta gating exit codes" -ripwire recall — "quality delta gating exit codes" — 107 relevant of 200 document files, best-first — total=107 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2337 est_tokens=5591 - -━━ skills/ripwire-quality-bar/SKILL.md (relevance 6.582) ━━ [sections: 1 of 10 selected (10 in doc), section-granular; whole doc 30349 B; lines="121-142"; dropped_by_budget=9] [truncated: 2098 of 9804 bytes] -## The loop -1. **Zero-setup path:** just make your change, then run `ripwire --quality-delta --legend=compact` - before you call it done — add `--legend=compact` every time you run this in a loop: on a CLEAN report the - legend is nearly the whole payload (2,776 B to 454 B measured on a small fixture, 15,601 B to 8,775 B on - a mid-change repo), the rows are byte-identical either way, and you have already read the dictionary — in a git repo it auto-compares the working tree vs `git HEAD` (`` confirms it), no start-of-task action needed. **Tighter loop on a long change:** run - `ripwire --quality-baseline` FIRST — **on a clean tree** — to pin an explicit floor (takes - precedence over HEAD) so each edit deltas against the original start, not the last commit. On a tree that - already differs from HEAD the pin **refuses**, naming the gating findings it would have swallowed into the - floor: commit first, or pass `--allow-dirty` to pin anyway, which stamps the absorbed count so every later - report carries `baseline_absorbed="N"` and a green exit beside it reads "clean *since the pin*". +ripwire recall — "quality delta gating exit codes" — 107 relevant of 200 document files, best-first — total=107 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2337 est_tokens=5372 + +━━ README.md (relevance 8.863) ━━ [sections: 2 of 21 selected (60 in doc), section-granular; whole doc 180569 B; lines="2426-2437,2438-2445"; dropped_by_budget=19] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | ... [17 more line(s); run it to see the whole thing] ``` @@ -541,25 +544,20 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1790108-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1870195-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=947 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2041 est_tokens=951 -━━ commands.md (relevance 10.605) ━━ [sections: 1 of 150 selected (179 in doc), section-granular; whole doc 695399 B; lines="496-521"; dropped_by_budget=149] [truncated: 1854 of 4242 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 9.817) ━━ [sections: 1 of 151 selected (180 in doc), section-granular; whole doc 703917 B; lines="3641-3645"; dropped_by_budget=150] [truncated: 1865 of 7256 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +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 … [line truncated: 1355 more bytes on this line] -... [17 more line(s); run it to see the whole thing] +(capped: 1 of 2 relevant document files omitted — raise --max-tokens or narrow the query for 1 more (~2548-byte budget)) ``` ### `--tree` @@ -576,7 +574,7 @@ _File-by-file orientation map (top symbols per file)._ $ ./build/ripwire . --tree - + @@ -684,10 +682,10 @@ $ ./build/ripwire . --no-stable --top-k=3 - - + + - + @@ -718,8 +716,8 @@ $ ./build/ripwire . --around=rankGraphTeleport - - + + @@ -764,7 +762,7 @@ _What SYM calls (1-hop out-edges)._ $ ./build/ripwire . --callees=rankGraphTeleport - + @@ -797,15 +795,15 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count $ ./build/ripwire . --uses=rankGraphTeleport - + - - - - - + + + + + ``` @@ -831,19 +829,19 @@ _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] ``` @@ -868,19 +866,19 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - - - - - - - - - - - - + + + + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -904,11 +902,11 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d $ ./build/ripwire . --path=main,rankGraphTeleport - - - - - + + + + + ``` @@ -928,7 +926,7 @@ $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + @@ -954,16 +952,16 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - + - + @@ -1037,9 +1035,9 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - + - + @@ -1072,7 +1070,7 @@ _Which symbols a TEST FILE exercises — the reverse direction of --affected._ ``` $ ./build/ripwire . --exercises=test/regression.sh - + @@ -1099,7 +1097,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 3840b6cb8 +at: f8d431396 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1117,19 +1115,19 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - - + + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1153,7 +1151,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1176,7 +1174,7 @@ _Regex search + enclosing symbol._ ``` $ ./build/ripwire . --regex='fnv1a\w+' - @@ -1205,7 +1203,7 @@ _Same search with one line of source context either side._ ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-context=1 - @@ -1234,7 +1232,7 @@ _Boolean grep: hits where BOTH literals share the matched line (--grep-scope=lin ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache - @@ -1244,7 +1242,7 @@ $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache - + ... [17 more line(s); run it to see the whole thing] @@ -1263,7 +1261,7 @@ _Drop every hit in a file that ALSO contains the --not literal anywhere (file sc ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file - @@ -1294,7 +1292,7 @@ _Drop every hit in a file that ALSO contains the --not literal anywhere (file sc ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file - @@ -1327,7 +1325,7 @@ _Span tiers off: the exhaustive view — the comment and string hits the default ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-in=any - @@ -1364,7 +1362,7 @@ _h= on each editable enclosing-symbol row: a freshness-pinned identity an edit v $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --handles - @@ -1397,7 +1395,7 @@ _Tree-sitter structural query WITHOUT a capture — a bare node query gets a cap ``` $ ./build/ripwire . --match='(if_statement)' - + if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench @@ -1452,13 +1450,13 @@ $ ./build/ripwire . --query="teleport pagerank" --top-k=5 - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1480,17 +1478,17 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + - - + + 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 RankDisclosure - + What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph - + 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… ... [19 more line(s); run it to see the whole thing] ``` @@ -1521,10 +1519,10 @@ $ ./build/ripwire . --pack-signatures --top-k=10 - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -1612,11 +1610,11 @@ _Comments stripped + blank runs collapsed — compressBody is the function that ``` $ ./build/ripwire . --expand=compressBody --top-k=0 --compress - + - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -1747,17 +1745,17 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - + + + - + + - ... [17 more line(s); run it to see the whole thing] @@ -1785,15 +1783,15 @@ $ ./build/ripwire . --hotspots - - - - - - - + + + + + + + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1812,11 +1810,11 @@ _Token-normalized duplicate bodies._ $ ./build/ripwire . --clones - - - - - + + + + + @@ -1843,15 +1841,15 @@ _Per-function readability, LEAST readable first (Halstead volume, token entropy, $ ./build/ripwire . --readability --limit=8 - + - - + + - - + + - + ``` @@ -1877,7 +1875,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1913,17 +1911,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1944,17 +1942,17 @@ $ ./build/ripwire . --quality-panel - + + + + + - - - - - - - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1980,13 +1978,13 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha $ ./build/ripwire . --context-ratio --limit=8 - - - - - - - + + + + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2011,7 +2009,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2022,7 +2020,7 @@ $ ./build/ripwire . --naming-calibration

-

+

... [13 more line(s); run it to see the whole thing] ``` @@ -2047,11 +2045,11 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio $ ./build/ripwire . --naming-consistency --limit=8 - - - - - + + + + + @@ -2083,7 +2081,7 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - + @@ -2092,10 +2090,10 @@ $ ./build/ripwire . --lint --naming-locals - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2118,7 +2116,7 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + @@ -2153,16 +2151,16 @@ $ ./build/ripwire . --cochange - + - - - - + + + + - + ... [22 more line(s); run it to see the whole thing] ``` @@ -2187,16 +2185,16 @@ $ ./build/ripwire . --cochange --cochange-recur=2 - + - - - + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2215,16 +2213,16 @@ $ ./build/ripwire . --cochange --cochange-groups - - + + - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2245,16 +2243,16 @@ $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2363,7 +2361,7 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - + @@ -2372,10 +2370,10 @@ $ ./build/ripwire . --lint - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2473,7 +2471,7 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2482,10 +2480,10 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2560,15 +2558,15 @@ _Cluster the call graph into cohesive modules._ $ ./build/ripwire . --communities - + - + - + @@ -2591,7 +2589,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp $ ./build/ripwire . --community=0 - ``` @@ -2616,17 +2614,17 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - - + + + - - + + - + - + ... [17 more line(s); run it to see the whole thing] @@ -2653,16 +2651,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2033 files · 18474 symbols · 21771 edges · 1285 modules (11480 call-graph isolated) +2039 files · 18699 symbols · 22330 edges · 1313 modules (11420 call-graph isolated) Root: `.` -Call-graph isolate provenance: 2608 declaration, 1150 header, 3637 source, 4085 document; 0 connected Louvain singletons +Call-graph isolate provenance: 2633 declaration, 1162 header, 3670 source, 3955 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 1285) -- **src::emitTo@infra/emit.h:46:2380 [write,run,emit]** — 721 symbols -- **src::append@elixir_resolve.h:111:4875 [resolve,compute,collect]** — 713 symbols -- **src::formatTo@infra/emit.h:134:8476 [parse,resolve,read]** — 508 symbols +## Modules (call-graph clusters; showing 12 of 1313) +- **src::assign@infra/svector.h:342:19905 [resolve,compute,collect]** — 652 symbols +- **src::emplace@infra/svector.h:408:22477 [resolve,parse,run]** — 617 symbols +- **src::emitTo@infra/emit.h:51:2620 [write,run,emit]** — 377 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2680,12 +2678,12 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ $ ./build/ripwire . --seams - - - + + - + + @@ -2710,11 +2708,11 @@ $ ./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"] - n98["src
5351"] - n99["src/infra
621"] + n98["src
5422"] + n99["src/infra
630"] end subgraph sg1 ["test"] - n100["test
3467"] + n100["test
3546"] n269["test/fixtures/recallpassage
314"] n255["test/expandmodefix
151"] n257["test/expandsibsfix
149"] @@ -2741,14 +2739,14 @@ $ ./build/ripwire . --owners - - + + - + - - + + @@ -2770,8 +2768,8 @@ _High-confidence internal functions with no caller. NOTE the filter is a path-CO ``` $ ./build/ripwire . --dead-code=src - - + + ``` @@ -2835,7 +2833,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2872,7 +2870,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - @@ -2909,7 +2907,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -2979,7 +2977,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3009,12 +3007,13 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - + - - + + + ``` @@ -3058,15 +3057,15 @@ _Insert BEFORE, with --edit-target-file pinning which same-named definition (her ``` $ ./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":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 744 more bytes on this line] +{"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":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 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":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":670,"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3081,12 +3080,12 @@ _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 bec ``` $ ./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":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 641 more bytes on this line] -"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"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":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":670,"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3128,15 +3127,15 @@ _Insert BEFORE, with --edit-target-file pinning which same-named definition (her ``` $ ./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":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 744 more bytes on this line] +{"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":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 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":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], -"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":670,"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3195,11 +3194,11 @@ _The same plan committed: per-file locks, re-verify-before-write, atomic rename, ``` $ ./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":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multi … [line truncated: 525 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multi … [line truncated: 536 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":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true}}]} +"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3244,7 +3243,7 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - + @@ -3282,7 +3281,7 @@ _Forward flow: which statements the seed's value reaches, at the default depth b $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - + teleport( pw.begin(), pw.end() );]]> @@ -3362,7 +3361,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - + ``` @@ -3388,11 +3387,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3462,12 +3461,12 @@ Per local ref (SUBSTR filters ref names): the lines its own divergent work AUTHO **Try it** -_Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts._ +_Which refs of the one branch `main` (no ref family left to select on this checkout) still hold divergent authored work vs HEAD, with verdicts._ ``` -$ ./build/ripwire . --stray-content=lane/ +$ ./build/ripwire . --stray-content=main - + ``` @@ -3487,12 +3486,12 @@ $ ./build/ripwire . --stray-content=lane/ **Try it** -_Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root)._ +_Select the genuinely-unmerged refs of the one branch `main` (no ref family left to select on this checkout) and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root)._ ``` -$ ./build/ripwire . --stray-content=lane/ --plan +$ ./build/ripwire . --stray-content=main --plan - + ``` @@ -3511,12 +3510,12 @@ SCOPE is what each ref AUTHORED — the paths `git diff base..tip` reports again **Try it** -_Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all._ +_Cross-branch ABI-break gate over the one branch `main` (no ref family left to select on this checkout): struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all._ ``` -$ ./build/ripwire . --stray-content=lane/ --abi +$ ./build/ripwire . --stray-content=main --abi - + ``` @@ -3539,12 +3538,12 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - + + + + + ... [24 more line(s); run it to see the whole thing] ``` @@ -3570,7 +3569,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3651,8 +3650,8 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - + @@ -3686,17 +3685,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - + + + + + + + + + + - - - - - - ... [19 more line(s); run it to see the whole thing] ``` @@ -3721,17 +3720,17 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - - - - + + + + + + + + + + - - - - - - ... [19 more line(s); run it to see the whole thing] ``` @@ -3755,18 +3754,18 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - - - - + + + + + + + + + + + - - - - - - ... [18 more line(s); run it to see the whole thing] ``` @@ -3791,7 +3790,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3828,8 +3827,8 @@ 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:3406 #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:3447 - #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1158 - #3 0x102e01a44 in main src/main.cpp:2988 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1441 + #3 0x102e01a44 in main src/main.cpp:3475 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -3856,7 +3855,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -3946,21 +3945,19 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 104 more bytes on this line] - + +knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedBy makes in full above, which applies here verbatim: firstFlagOutside() walks the rows parseArgs itself minline constexpr std::string_view kInRideAlong[] = + 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 ) noexcept -struct AgentTarget - -verifier N2/N3/N11: the bad-VALUE refusal tablestruct McpValueSpec - -... [17 more line(s); run it to see the whole thing] +... [19 more line(s); run it to see the whole thing] ``` **Shaped by:** `--top-k`, `--token-budget`, `--for`, `--test-gate`, `--expand`, `--compress`, `--no-redact`, `--partition` @@ -3983,20 +3980,21 @@ _Fan-out form: one shared core + 3 per-agent slices carved along call-graph comm ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3 - - - + + + - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const std::vector<std::string>& verbLines… - -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 ) noexcept -... [18 more line(s); run it to see the whole thing] + +knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedB…inline constexpr std::string_view kInRideAlong[] = + +... [17 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -4017,17 +4015,17 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + - - + + 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 RankDisclosure - + What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph - + 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… ... [19 more line(s); run it to see the whole thing] ``` @@ -4093,14 +4091,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + ``` @@ -4126,14 +4124,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4161,7 +4159,7 @@ $ ./build/ripwire . --skipped - @@ -4247,10 +4245,10 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 - - + + - + ... [16 more line(s); run it to see the whole thing] ``` @@ -4273,22 +4271,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~7 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - -... [8 more line(s); run it to see the whole thing] + + + + + + + + +... [16 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend` @@ -4350,7 +4348,7 @@ _The same gating report under --legend=compact — same rows, same exit 2, schem ``` $ ./build/ripwire . --quality-delta --legend=compact - @@ -4400,17 +4398,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4439,10 +4437,10 @@ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - - + + - + @@ -4471,10 +4469,10 @@ $ ./build/ripwire . --map-diff --top-k=5 - - + + - + ... [11 more line(s); run it to see the whole thing] @@ -4504,10 +4502,10 @@ $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - - + + - + @@ -4554,10 +4552,10 @@ $ ./build/ripwire . --no-cache --top-k=3 - - + + - + @@ -4585,10 +4583,10 @@ $ ./build/ripwire . --no-ignore --top-k=3 - - + + - + @@ -4619,8 +4617,8 @@ $ ./build/ripwire . --max-file-size=8K --top-k=3 - - + + @@ -4685,10 +4683,10 @@ $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - - + + - + @@ -4757,7 +4755,7 @@ _Labelled verdict-accuracy eval for --stray-content — three labels over REAL l ``` $ ./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) -lane/sc-legend merged +no-such-ref-0 merged no-such-ref-1 unmerged no-such-ref-2 merged ``` @@ -4774,14 +4772,14 @@ _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 0.9% 4.4% 9.6% - BM25 12.3% 13.5% 14.5% - BM25sub 12.3% 15.9% 17.8% - BM25body 23.4% 37.4% 48.4% - fused 5.5% 14.5% 21.4% - anchored 22.0% 37.4% 48.4% - same-dir 1.5% 3.0% 5.4% - random 0.2% 0.5% 1.0% <- floor (random ranking over F=2033 files) + ripwire 0.5% 1.5% 6.7% + BM25 10.9% 13.1% 16.7% + BM25sub 10.7% 15.1% 20.2% + BM25body 22.3% 29.9% 34.9% + fused 4.2% 14.8% 22.0% + anchored 20.8% 29.9% 34.9% + same-dir 1.6% 5.6% 7.2% + random 0.2% 0.5% 1.0% <- floor (random ranking over F=2039 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 / @@ -4804,19 +4802,19 @@ _Known-item retrieval eval: MRR + recall@k per ranker per query mode._ ``` $ ./build/ripwire . --eval-retrieval ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) - sample: population=4115 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population + sample: population=4164 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population (smallest fnv1a64(scope::name) over the population, cut on the key so an identity is never split; path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) 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.714 58.0% 88.2% 92.7% - subtoken doc-phrase 0.930 90.9% 95.2% 95.7% - name-exact name 0.921 85.6% 97.2% 98.2% - name-exact doc-phrase 0.014 0.5% 2.3% 2.8% - anchored name 0.718 59.4% 87.3% 91.5% - anchored doc-phrase 0.926 90.2% 95.0% 95.7% - routed name 0.921 85.6% 97.2% 98.2% - routed doc-phrase 0.928 90.9% 94.9% 95.5% + subtoken name 0.714 57.9% 88.4% 92.9% + subtoken doc-phrase 0.932 91.2% 95.2% 95.8% + name-exact name 0.924 86.0% 97.4% 98.4% + name-exact doc-phrase 0.013 0.4% 2.2% 2.7% + anchored name 0.718 59.3% 87.4% 91.7% + anchored doc-phrase 0.927 90.5% 95.0% 95.8% + routed name 0.924 86.0% 97.4% 98.4% + routed doc-phrase 0.930 91.1% 95.0% 95.5% ... [6 more line(s); run it to see the whole thing] ``` @@ -4862,7 +4860,7 @@ _Version + short build info._ ``` $ ./build/ripwire --version -ripwire 0.6.0 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=88355acd0+dirty) +ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d431396) ``` **Shaped by:** `--impact`, `--verify`, `--metrics`, `--deps`, `--naming-locals`, `--arch`, `--dry-run`, `--doc-drift` diff --git a/docs/LINEAGE.md b/docs/LINEAGE.md index 9671457c..85dae021 100644 --- a/docs/LINEAGE.md +++ b/docs/LINEAGE.md @@ -21,7 +21,7 @@ licence in [`THIRD_PARTY.md`](../THIRD_PARTY.md). First-party code under `src/` third-party code lives under `third_party/` and keeps its own licence. Citing a paper means the idea was read and applied, not that any of its text or code is here. -**The counts, derived from the tables below:** **49 repositories** and **70 papers** are folded, and +**The counts, derived from the tables below:** **49 repositories** and **71 papers** are folded, and a labelled survey of **237 tools** contributed nothing and says so. **The two sets are disjoint by construction, so they add rather than nest:** a tool that contributed a lesson gets a row in §3a and is never repeated in §3b, which makes the field study 49 folded *plus* 237 surveyed — not 49 picked @@ -60,8 +60,9 @@ rejected rather than shipped on the paper's authority. Those are the LARGER row overlap row — LARGER was rejected before it ever shipped, and the overlap proxy was built, measured on this repository, and withdrawn. They are the two worth reading first. -Two rows rest on three sources that are not peer-reviewed — a vendor specification, a book, and a -practitioner article. Each is labelled as such in its own row rather than left to look like a paper. +Three rows rest on four sources that are not peer-reviewed — a vendor specification, a book, a +practitioner article and an online textbook chapter. Each is labelled as such in its own row rather than +left to look like a paper. | Work | Lesson taken | Where it lives | | --- | --- | --- | @@ -125,6 +126,7 @@ practitioner article. Each is labelled as such in its own row rather than left t | Paul, Helm, Glavaš & Gurevych, *OctoLong: Mid-Training on Cross-Repository Code Contexts Enhances Long-Context Modeling* — [arXiv:2608.05141](https://arxiv.org/abs/2608.05141) | A cross-repository context-curation pipeline is only as trustworthy as its linking discipline — OctoLong instruments an AST parser, a language server and a package manager to curate genuinely dependency-linked spans across repository boundaries rather than merely co-located ones. External corroboration of the same discipline applied here. | Multi-root workspaces (`ripwire ...`): cross-root edges admitted only on explicit include/import/FFI evidence, never on same-name coincidence (`src/resolve.h`) | | Shao, Zeng, Zhao & Yu, *HyperFL: Query-Adaptive Representation Learning for Software Fault Localization* — [arXiv:2608.02967](https://arxiv.org/abs/2608.02967) | Adapting the retrieval representation to the shape of the query, rather than serving every query from one fixed embedding space, is worth doing — HyperFL does it with a learned hypernetwork generating per-query LoRA parameters. External motivation, after the fact and without the learner, for choosing a representation deterministically instead. | The `--for` query-shape router: name-exact BM25 versus subtoken+body BM25, chosen per query with the evidence printed as `route=` (`src/lexical.h`, `src/filter.h`) | | Langdale & Lemire, *Parsing Gigabytes of JSON per Second* — [doi:10.1007/s00778-019-00578-5](https://doi.org/10.1007/s00778-019-00578-5) (VLDB Journal 2019) | Classify every byte of a block with two 16-entry shuffles — the low nibble indexes a column bitmap, the high nibble selects a row bit — and then reason about the text as bitmasks rather than bytes. | `classMasks` in `src/infra/strkern.h`, and the tokenizer that consumes its masks (`src/lexindex.h`: token starts and camel/acronym splits are mask algebra, not a per-byte state machine) | +| Slotin, *Algorithmica — Algorithms for Modern Hardware*, the S-tree chapter — [en.algorithmica.org/hpc/data-structures/s-tree](https://en.algorithmica.org/hpc/data-structures/s-tree/) *(an online textbook chapter, not peer-reviewed)* | The primitive inside a B-tree node is not "binary search the keys" but **count the keys less than x** — a branchless, fixed-width SIMD reduction. That inverts what node width is *for*: it becomes a property of the cache line and the vector lane count rather than of the comparison count, and the descent loses its branches. The chapter's tree is STATIC; what transfers is the scan kernel, not the structure. | `src/infra/dynamic_map.hpp` — the per-node `rank_lt` / `rank_le` kernel of the B+ tree that stands in for `std::map` and `std::unordered_map` everywhere in this tree (both are forbidden by CONTRIBUTING's container rule). NEON on AArch64, SSE2/SSE4.2 on x86_64, a portable scalar loop elsewhere; node width `B` is a template parameter constrained to a whole multiple of the lane count, default 16. What the static chapter has no use for and is designed here: insert and erase over node pools recycled through intrusive free lists with no per-operation allocation, the struct-of-arrays node layout and 32-bit child handles, the parent-link-free descent, the max-value sentinel scheme that stays correct when that value is a real key, and the less-or-equal rank variant a dynamic descent requires (`test/dynmapsimdcheck.sh`) | **Two co-change parameters here were derived independently and landed on the published values — and saying so is stronger than silence.** The bulk-commit cap (`kCoBoostMaxFilesPerCommit`, `src/gitmine.h`) diff --git a/docs/captures/COMMANDS_showcase_2026-09-14.md b/docs/captures/COMMANDS_showcase_2026-09-14.md new file mode 100644 index 00000000..dedd83c8 --- /dev/null +++ b/docs/captures/COMMANDS_showcase_2026-09-14.md @@ -0,0 +1,7102 @@ +# ripwire — every verb, run for real + +- **Date:** 2026-09-14 (regenerated capture; supersedes any older `docs/captures/COMMANDS_showcase_*.md`) +- **Lives in `docs/captures/`** — a directory the crawl/retrieval lenses SKIP (`kCrawlSkipDirs`, src/ingest.h): a generated doc that quotes every verb's output out-scores the source for any query about the tool and was measured at 77% of `--recall` on this repo when it sat at the root. `test/argvdiffcheck.sh` harvests its `## `-heading command lines as differential vectors — keep that format. +- **Version:** `ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d431396)` +- **Repo:** the ripwire repo @ `f8d43139` — **CLEAN — `git status --porcelain` is empty**. The diff-aware verbs (`--situ`/`--test-gate`/`--quality-delta`/`--pr-context`/`--map-diff`/`--edit-check`) answer a question about the WORKING TREE, so that condition is part of their answer and every one of their captions below states which tree it recorded against. A clean tree is the honest default for a showcase, so they appear TWICE: once here on the clean tree (their empty/exit-0 shape) and once in the final section against a throwaway `git clone --local` sandbox carrying one deliberate regression, so their real gating shapes are visible without writing a byte into the read-only repo. +- **Corpus:** the ripwire repo itself (dogfood), via `./build/ripwire` +- **Sandbox diff** (the last section only): `.ripwire_notes | 2 ++ + .ripwire_quality_acks | 1 + + src/infra/sortutil.h | 31 ++++++++++++++++++++++++++++--- + 3 files changed, 31 insertions(+), 3 deletions(-)` — one preexisting function made deeply nested, one function's arity changed 1 -> 2, one copy-paste duplicate helper, one new 8-parameter public function. + +**How to read the blocks:** ripwire's real XML output is minified — often ONE long line. For scanability, long minified lines are displayed re-wrapped with a line break at every tag seam (`><`). Header COMMENT lines (the legends) always appear in full — they are exempt from the per-line cut; any OTHER display line over 300 bytes is cut with a `… [line truncated: N more bytes]` marker, which can hit a long root element or row. `--plan-lanes` emits JSON and is re-wrapped at object seams the same way. Long outputs are cut to their first ~30 display lines with a `… [N more display lines; full output is M bytes]` marker giving the true size. Exit codes are recorded when non-zero; wall time when >1s. + +**Not run (and why):** `ripwire ` (network clone), `--listen` / `--mcp-token` / `--allow-remote-edits` (the HTTP-server posture; `--mcp` itself IS captured in its own section as a one-shot stdio JSON-RPC exchange, and `wrap claude` shows the wiring), `--arch --baseline[-update]` (state writer against the read-only repo — `--note-add` / `--quality-baseline` / `--quality-ack` / the three edit verbs / `--edit-plan` ARE shown, inside the throwaway sandbox clone; `--index-out` / `--pin-census` write to scratch), `--eval-mined` (needs a `minedpair.jsonl` artifact from `bench/mine_traces.py`; none present in the tree), `--refetch` (git-url only), `--force` (wrap-only modifier), `--scan-skills` bare form (would sweep `~/.claude/skills`; the explicit-DIR form is shown instead), `--help` (198 lines — read it from the binary). + + +--- + +# understand a codebase cold + +## `./build/ripwire .` + +*The default ranked symbol map — start here when landing cold in a repo.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [813 more display lines; full output is 22661 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --top-k=0 --expand=rankGraphTeleport` + +*NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* + +````` + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --token-budget=100` + +*GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* + +**exit code: 3** + +````` + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9136 > budget=100 +````` + +## `./build/ripwire . --for="incremental cache invalidation when a file content hash changes"` + +*The task lens: ranked signatures + quality metrics framed for the task.* + +````` + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex + +the cached index for `root`, rebuilt only when stale (otherwise returned as-is, no parse, no gra…inline const McpIndex& getIndex( const std::string& root ) +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFileScan& … [line truncated: 11 more bytes on this line] +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline std::uint64_t blobChecksum( std::string_view s ) noexcept +inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… +inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes, const st … [line truncated: 15 more bytes on this line] +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, … [line truncated: 54 more bytes on this line] +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t>& fi … [line truncated: 16 more bytes on this line] +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const … [line truncated: 90 more bytes on this line] +struct IngestResult + + + +… [68 more display lines; full output is 10174 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +````` + + + + + +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 +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + +& 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 ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { +… [12 more display lines; full output is 5509 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --no-route` + +*Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* + +````` + + + + + +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… +struct RankedGraph +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago", boo … [line truncated: 10 more bytes on this line] +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… +inline std::string didYouMean( const IngestResult& ing, std::string_view name ) +inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = nullptr ) +inline const McpIndex& getIndex( const std::string& root ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double&g … [line truncated: 36 more bytes on this line] +inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = nullptr )inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +struct Graph +## Coverage inventory +### The rank wrappers — `test/verify_pagerank.cpp` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +… [137 more display lines; full output is 16178 bytes on 88 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` + +*T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* + +````` + + + + + +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 +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --adaptive` + +*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]).* + +````` + + + + + +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 * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() + +errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 48> kLangTable = +inline bool refuseKotlinNesting( const LangEntry& le, std::string_view bytes, const char* path, std::size_t fileId, IngestFileScan& scan ) +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline SymTreeIndex indexCommittish( const std::string& root, const std::string& committish, const std::vector<std::string>& excludes, std::size_t maxFileBytes,…std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& … [line truncated: 8 more bytes on this line] +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +struct LintRule +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [53 more display lines; full output is 9889 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` + +*Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* + +````` + + + + +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 ) + +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 ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct LensRanking +struct NameCorpusStats +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the WH … [line truncated: 56 more bytes on this line] +struct CandidateProvenance +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline constexpr int kLexWeightDoc = 2 +struct RecallSectionPick +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline bool canonicalIdMatches( std::string_view canonAbs, std::string_view spec ) noexcept +… [56 more display lines; full output is 10137 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` + +*Same task with the anchor disabled — the contrast the flag exists for.* + +````` + + + + +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 ) + +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 ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct LensRanking +struct NameCorpusStats +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +struct Bm25Params +inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noR … [line truncated: 18 more bytes on this line] +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail reads the WH … [line truncated: 56 more bytes on this line] +struct CandidateProvenance +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +inline constexpr int kLexWeightDoc = 2 +struct RecallSectionPick +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline bool canonicalIdMatches( std::string_view canonAbs, std::string_view spec ) noexcept +… [58 more display lines; full output is 10223 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lego=Vehicle` + +*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 . --exemplar="format byte sizes for humans"` + +*The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* + +````` + + + + + f, A&&... a ) +{ + // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; + // `{}` on a char[N] is a different question that library versions answer differently, and an + // implementation that formats the ARRAY emits the trailing NUL and the uninitialised bytes after it. + // A regex sweep missed sites twice, so the compiler enforces it instead of a reviewer. + // Only a MUTABLE char[N] is rejected. A string literal is const char[N]; every implementation formats + // that as a string, and the hazard here is the reusable buffer that was written short. + static_assert( ( ... && !( std::is_array_v> + && !std::is_const_v>> ) ), + "pass rw::cstr( buf ) for a fixed char buffer: {} on a char[N] is not printf's %s" ); + std::fputs( std::format( f, std::forward( a )... ).c_str(), stream ); +}]]> +````` + +## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` + +*Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* + +````` + + + + +ripwire '.' --verify='calls(runDefaultMap, rankGraphTeleport)' --legend=compact + + +````` + +## `./build/ripwire . --help-task="write a cheerful release announcement"` + +*The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +````` +ripwire recall — "quality delta gating exit codes" — 107 relevant of 200 document files, best-first — total=107 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2337 est_tokens=5372 + +━━ README.md (relevance 8.863) ━━ [sections: 2 of 21 selected (60 in doc), section-granular; whole doc 180569 B; lines="2426-2437,2438-2445"; dropped_by_budget=19] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ skills/ripwire-quality-bar/SKILL.md (relevance 6.593) ━━ [sections: 1 of 10 selected (10 in doc), section-granular; whole doc 30349 B; lines="121-142"; dropped_by_budget=9] [truncated: 2098 of 9804 bytes] +## The loop +1. **Zero-setup path:** just make your change, then run `ripwire

--quality-delta --legend=compact` + before you call it done — add `--legend=compact` every time you run this in a loop: on a CLEAN report the + legend is nearly the whole payload (2,776 B to 454 B measured on a small fixture, 15,601 B to 8,775 B on +… [167 more lines, 13710 bytes total] +````` + +## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` + +*The directory-as-knowledge-base pattern: --recall pointed at a 1870195-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* + +````` +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2041 est_tokens=951 + +━━ commands.md (relevance 9.817) ━━ [sections: 1 of 151 selected (180 in doc), section-granular; whole doc 703917 B; lines="3641-3645"; dropped_by_budget=150] [truncated: 1865 of 7256 bytes] +### `--field-affinity[=STRUCT]` + +**Answers:** find fields that are read together but declared far apart — the cache-locality lens 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 … [line truncated: 1355 more bytes on this line] + +(capped: 1 of 2 relevant document files omitted — raise --max-tokens or narrow the query for 1 more (~2548-byte budget)) +````` + +## `./build/ripwire . --tree` + +*File-by-file orientation map (top symbols per file).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [370 more display lines; full output is 11781 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +````` +(empty) +````` + +Artifact written: + +````` + 158946 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + + +--- + +# navigate / answer a question + +## `./build/ripwire . --around=rankGraphTeleport` + +*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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [228 more display lines; full output is 10078 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` + +*The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [3182 more display lines; full output is 85022 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` + +*The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [37 more display lines; full output is 5251 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --legend=compact` + +*The same rows under --legend=compact: the prose legend becomes one compact comment plus schema="ripwire.callers/v1" on the root — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB of legend gone. Works on EVERY XML verb now, not four.* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [25 more display lines; full output is 5302 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface` + +*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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5547 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface --include-builtins` + +*The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5511 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --path=main,rankGraphTeleport` + +*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 . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [46 more display lines; full output is 7893 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --situ` + +*Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: f8d431396 + (0 changed files — working tree is clean, nothing to analyze) +````` + +## `./build/ripwire . --test-gate` + +*Pre-PR gate on a CLEAN tree: no obligations, exit 0.* + +````` + + +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT` + +*Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + += kType3MaxPairs ) { DEGRADED_PATH_ALERT( "clones: Type-3 pair cap hit — first N compared (both-gate-surviving) near-misses kept, rest skipped" ); goto done; }]]> + + + + +… [565 more display lines; full output is 35238 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-context=1` + +*Same search with one line of source context either side.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1171 more display lines; full output is 45404 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-before=1 --grep-after=2 --limit=3` + +*The asymmetric spelling of the same context: one line before and two after each hit (ripgrep's -B/-A), on a three-hit window.* + +````` + + + + + + + + +, each carrying its LINE (l=), its matched text as the hit's own CDATA (line_bytes= rides a row whose line was too long to print whole and gives that WHOLE line's byte length — absent means the CDATA IS the whole line) and enclosing symbol (in=, a NAME here; the same spelling is a fan-in COUNT in for/pack-task/exemplar; ABSENT (never an empty in= value) when no symbol encloses the hit, which is NOT the same claim as file scope — and on a file row carrying parse_degraded="1" it is NO CLAIM AT ALL: that file's parse holds ERROR/MISSING nodes (the skipped verb itemizes err=/err_ratio=), symbols there may be unextracted, so read in= absence inside it as UNKNOWN, not as file scope; absence of parse_degraded= on a row means the parse was clean, except that a file the ingest never parsed at all — doc-format, binary-sniffed, unreadable — is also unmarked, the skipped verb's unmeasured class). root= on the root element is the crawl root every is now RELATIVE to (single-root runs only; absent ⇒ p= is the path ingest itself used, unchanged). ORDER: SOURCE files before test/bench files before docs, then path and line. shown=/capped= = rows printed vs found (a count of underlying HITS, the same unit hits= uses, not of printed elements); hits_capped="1" ⇒ hits= is a FLOOR (collection budget reached) and the root then also carries counts_floor="1" and capped="1" — rows exist that no page holds. SPAN TIERS: each hit is classified by the tree-sitter span it sits in (code/comment/string) and this answer serves the CODE tier, or — when no hit is code — comment and string TOGETHER; tier= names what was served when it is not code, so a pattern living only in prose is answered, never emptied. tier_partial= (value 1, present only then) qualifies that label: it was elected over the CLASSIFIED hits ALONE while tier_unclassified= hits were never classified, so read it as the tightest tier PROVEN present, never as proof that no hit is code. Nothing past the budget is suppressed, so the partiality narrows what the LABEL may be read to mean, never which rows you got; its absence beside a tier= means the label is a fact. suppressed_comment=/suppressed_string= are the classified hits held back: not in hits=, and the reason complete= cannot appear. Pass grep-in=any (dashes omitted) for every tier. Hit files are parsed on demand under a fixed budget: tier_parsed= how many were classified, tier_budget= which ceiling stopped it (files or bytes, present only then — and the root then also carries counts_floor="1": the tier counts are floors while hits= stays exact and every row is served), tier_unclassified= hits in files nothing classified — always EMITTED, never suppressed. A byte-identical match at OTHER sites in the SAME file folds into the first 's n= (default 1, unset) and an at-tagged sibling element (l=/in=, self-closing) per extra site — never on a paged or grep-context/-before/-after answer (dashes omitted, illegal in an XML comment), where every site keeps its own . After the hit rows, rows list each DISTINCT enclosing symbol NAME of THIS page (first-appearance order, bounded by the page) with callers= its 1-hop DISTINCT-caller count, unioned across same-named defs like the callers verb (a FLOOR — dynamic dispatch contributes no edge), defs= how many defs the name grouped (only when more than one), cx= complexity; amp=/tested= join only when a metrics co-run already computed that lens. On a zero-hit answer a element may follow: SUGGESTIONS, never matches — near= the nearest indexed symbol name (did-you-mean), next= a ready-to-paste conceptual fallback; absent for regex/non-word-like patterns or when nothing plausible exists. COMPLETENESS: complete= on the root (value 1) means this listing is EXHAUSTIVE and a consumer need not re-derive it: a LITERAL scan read every indexed file end to end, hit no collection ceiling, and printed every hit it found — so on this answer a zero really is zero and a hit absent above is absent from every indexed file. The claim is complete-within-the-index ONLY: most files the ingest skipped were never scanned (the skipped verb lists exactly which, with reasons; the ONE exception is the unindexed_files_scanned= class right below, itself never covered by complete=), and files outside the indexed roots are outside the claim. The largest single subtraction is named on the root itself: corpus_pruned_dirs= below counts the subtrees the built-in crawl denylist removed WHOLE, so read this claim as exhaustive over what was indexed, never over what is on disk. It never appears on a regex answer (the prefilter is a performance switch that may not change the answer, so neither mode claims), a capped or paged listing, or a scan that could not read a file; its ABSENCE claims nothing. The enc rows' caller counts stay FLOORS regardless — complete= speaks for the hit rows alone. unindexed_files_scanned= counts files outside the index (unsupported-ext, but text-looking — the skipped verb's own unsupported-ext class) that THIS answer additionally scanned for the same pattern; their hits print inside a trailing unindexed element (present only when it found something), holding its own rows in the same shape as above, and never carry in= — there is no symbol table to check for such a file, which is not the same claim as file scope. THE TWO POPULATIONS: every count above — files, hits, shown, capped, total, complete — is the IN-INDEX search ALONE. unindexed_hits= sizes the second one, always stated (a zero included); the trailing unindexed element carries that same number as its own count= beside shown=/capped= and obeys the SAME window limit/offset set here (dashes omitted), so a one-row page is one row on BOTH lists and a page past its end is empty, not repeated. unindexed_files_skipped= (present only when nonzero) counts candidates this scan saw but did not read: over the max-file-size ceiling, sniffed binary, or unreadable. unindexed_candidates_capped="1" (present only when true) means the CANDIDATE list itself (the skipped verb's own 500-row-per-class cap) was already a floor, so files past it were never considered here either — see the skipped verb for every row. corpus_excluded= counts files a caller's own exclude filter kept OUT of the index entirely; corpus_oversize= counts files the crawl SAW but dropped for exceeding the size ceiling; corpus_pruned_dirs= counts the DIRECTORIES the BUILT-IN crawl denylist pruned whole — vendor, third_party, build, dist, out, target, node_modules and the rest — a directory count and not a file count, because files beneath a pruned subtree are never stat'd and so were never counted. All three answer what an otherwise-empty answer alone cannot: not in this repo, or in a file that was never scanned — the skipped verb itemizes the rows behind the first two and reports the third as its own pruned_dirs=. next= is the one pasteable follow-up: the at verb on the top hit; the next page (compact legend) when cut; the conceptual lens on a zero-hit answer. raise the default cap with limit=N (offset=M pages; a cut listing carries total=/has_more=/next_offset= so a paging loop can continue from it); on the root, limit="0" means no explicit limit was given and the verb's own default page size shaped the window — never a zero-row page --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [395 more display lines; full output is 25342 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --match='(if_statement)'` + +*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.* + +````` + + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if 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["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 17172 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --match='(if_statement) @i'` + +*The same shape query WITH an explicit capture — identical hits, no auto_captured= attribute.* + +````` + + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if 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["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 17154 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --query="teleport pagerank" --top-k=5` + +*Raw BM25 ranking (debug lens; --for is the real verb).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [11 more display lines; full output is 3605 bytes on 1 raw line(s)] +````` + + +--- + +# zoom the detail ladder + +## `./build/ripwire . --for="pagerank power iteration" --detail=2` + +*Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* + +````` + + + + + +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 RankDisclosure + +What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph + +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… +struct PageRankRun +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "s… +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> … [line truncated: 13 more bytes on this line] +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +## Background — read these, in this order +## Design space and constraints +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +struct ColdParseReserve +std::vector<std::uint32_t> denseIdOfRoot( symbolCount, UINT32_MAX ) +probe_args() +void finalizeCrawlSkips( CrawlSkips& skips, const HashMap<std::string, std::uint64_t>& extTally ) +## The arithmetic — derive it from the … [line truncated: 44 more bytes on this line] +… [122 more display lines; full output is 14738 bytes on 77 raw line(s)] +````` + +## `./build/ripwire . --pack-signatures --top-k=10` + +*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: 91.8% fewer bytes at top-10, 85.2% at top-50, 85.5% at top-100 (re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [141 more display lines; full output is 13021 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` + +*Control-flow skeleton of one symbol, payload-only via the new --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 ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +## `./build/ripwire . --outline=rankGraphTeleport:1-10 --top-k=0` + +*CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* + +````` +& 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 ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +stderr: + +````` +ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — outlining the whole symbol (use --expand=rankGraphTeleport:1-10 for a body slice) +````` + +## `./build/ripwire . --expand=rankGraphTeleport --top-k=0` + +*Full body + inline callee signatures.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [24 more display lines; full output is 6567 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [838 more display lines; full output is 22007 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [16 more display lines; full output is 6608 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [311 more display lines; full output is 18667 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [16 more display lines; full output is 6604 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --arch=test/archfix/rules.txt` + +*Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [410 more display lines; full output is 60694 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [692 more display lines; full output is 71129 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +````` + + + + +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf + +````` + +stderr: + +````` +ripwire: lint-rules: test/lintrulesfix/rules/malformed.yaml:5: expected 'key: value' — file skipped +[math degraded] lint-rules: malformed rule file skipped (lintrules.h:393, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*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.* + +Input file: + +````` +#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 +````` + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 6520 bytes on 1 raw line(s)] +````` + +The joined finding — past the display cut above, extracted so the join is visible: + +````` +p = p->next +````` + +## `./build/ripwire . --communities` + +*Cluster the call graph into cohesive modules.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [189 more display lines; full output is 17180 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom` + +*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=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [137 more display lines; full output is 8382 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom --zoom-levels=3` + +*The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [247 more display lines; full output is 12216 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2039 files · 18699 symbols · 22330 edges · 1313 modules (11420 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 2633 declaration, 1162 header, 3670 source, 3955 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 1313) +- **src::assign@infra/svector.h:342:19905 [resolve,compute,collect]** — 652 symbols +- **src::emplace@infra/svector.h:408:22477 [resolve,parse,run]** — 617 symbols +- **src::emitTo@infra/emit.h:51:2620 [write,run,emit]** — 377 symbols +- **src::append@elixir_resolve.h:111:4875 [pack,write,run]** — 314 symbols +- **src::DEGRADED_PATH_ALERT@infra/Diagnostics.h:186:10129 [read,run,compute]** — 201 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:8566 [pack,allow,insert]** — 170 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [emit,collect,append]** — 158 symbols +- **src::str@ingest_cache.h:1500:142030 [read,write,scan]** — 54 symbols +- **src::resolveAtSeed@graph.h:3677:212634 [resolve,call,join]** — 52 symbols +- **src::PROFILE_SCOPE_DESCRIBE@infra/profileScope.h:1326:45284 [emit,check,resolve]** — 48 symbols +- **src::buf@infra/svector.h:125:8942 [insert,pack,move]** — 41 symbols +- **src/infra::active@profilePmc.h:845:34910 [print,ensure,report]** — 40 symbols + +## God files (most depended-on; showing 10 of 354) +- `src/infra/emit.h` — 84 dependents +- `src/model.h` — 80 dependents +- `src/infra/Diagnostics.h` — 48 dependents +- `src/serialize.h` — 35 dependents +- `src/graph.h` — 33 dependents +… [32 more lines, 3591 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [93 more display lines; full output is 12679 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +````` +%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. +flowchart LR + subgraph sg0 ["src"] + n98["src
5422"] + n99["src/infra
630"] + end + subgraph sg1 ["test"] + n100["test
3546"] + n269["test/fixtures/recallpassage
314"] + n255["test/expandmodefix
151"] + n257["test/expandsibsfix
149"] + n315["test/massfix
77"] + n297["test/legofix
60"] + n335["test/optremarksfix
59"] + n399["test/sliceflowsensfix
59"] + end + n71["docs
779"] + n1[".github
562"] + subgraph sg4 ["bench"] + n4["bench
539"] + n5["bench/agentloop
323"] + n49["bench/locbench/results/r5_pooling
235"] + n50["bench/locbench/results/r6_expansion
185"] + n57["bench/recalleval
108"] + n43["bench/locbench
104"] + n38["bench/headtohead/r3-headroom-2026-08-03
100"] + n63["bench/skillrater/results/2026-09-07/arms
98"] + n45["bench/locbench/results/r1cpp_anchorhop
92"] + n60["bench/shotgun
91"] + n56["bench/nestcal/r1-2026-08-07
84"] +… [21 more lines, 1591 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [757 more display lines; full output is 88875 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --dead-code=src` + +*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 . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +````` + + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +````` + + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 5.25s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 12739 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --edit-check=rankGraphTeleport` + +*Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +````` + + + + +````` + +## `./build/ripwire . --pr-context=HEAD~1` + +*The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*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).* + +**wall time: 3.73s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=main` + +*Which refs of the one branch `main` (no ref family left to select on this checkout) still hold divergent authored work vs HEAD, with verdicts.* + +````` + + + +````` + +## `./build/ripwire . --stray-content=no-such-ref` + +*A second selection, `no-such-ref`, which no local ref carries, so the verb REFUSES (exit 1), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --stray-content=no-such-ref matches no local ref — a zero here would be a failure, not a measurement + (the filter is a substring match against refs/heads names; run bare --stray-content to list them, e.g. --stray-content=feat/) +````` + +## `./build/ripwire . --stray-content=main --plan` + +*Select the genuinely-unmerged refs of the one branch `main` (no ref family left to select on this checkout) and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* + +````` + + + +````` + +## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` + +*A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zero here would be a failure, not a measurement + (the filter is a substring match against refs/heads names; run bare --stray-content to list them, e.g. --stray-content=feat/) +````` + +## `./build/ripwire . --stray-content=main --abi` + +*Cross-branch ABI-break gate over the one branch `main` (no ref family left to select on this checkout): struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* + +````` + + + +````` + +## `./build/ripwire . --whereis=rankGraphTeleport` + +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [34 more display lines; full output is 18469 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` + +*Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* + +**wall time: 3.93s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [339 more display lines; full output is 21527 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` + +*Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASA` + +*Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --flip: no gate named 'RIPWIRE_ASA' in . (did you mean 'RIPWIRE_ASAN'?) +ripwire: run `ripwire . --flags` for the gate table +````` + +## `./build/ripwire . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it with tests"` + +*NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* + +````` +{"v":1,"verb":"plan-lanes","at":"f8d431396","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2039,"symbols":18699,"edges":223 … [line truncated: 358 more bytes on this line] +"symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":278,"id":"./src/docdrift.h::DriftResult::DriftResult"}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2319,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2779,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, +{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","l":66,"id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":544,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1691,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/gitmine.h","n":"gitWindowBoundarySha","scope":"rw","key":"18627516699d7062","id":"./src/gitmine.h::rw::gitWindowBoundarySha","id_addressable":true,"id_collides_with":0,"l":1581,"ord":0,"overloads":1,"amb":3,"cx":3,"ccx":2,"churn":35,"tested":0}, +{"p":"./src/crossref.h","n":"writeWhereisPage","scope":"crossref","key":"3a0425468e6a18cf","id":"./src/crossref.h::crossref::writeWhereisPage","id_addressable":true,"id_collides_with":0,"l":2090,"ord":0,"overloads":1,"amb":11,"cx":22,"ccx":23,"churn":27,"tested":0}, +{"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2555,"ord":0,"overloads":1,"amb":4,"cx":6,"ccx":6,"churn":20,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1148,"ord":0,"overloads":1,"amb":2,"cx":7,"ccx":7,"churn":15,"tested":0}, +{"p":"./src/verbs_change.h","n":"runCrossRef","scope":"","key":"639de1c3670999f9","id":null,"id_addressable":false,"id_collides_with":0,"l":1500,"ord":0,"overloads":1,"amb":15,"cx":34,"ccx":69,"churn":38,"tested":0}, +{"p":"./src/docdrift.h","n":"docDriftNextAttr","scope":"docdrift","key":"7723ed789a1c9c09","id":"./src/docdrift.h::docdrift::docDriftNextAttr","id_addressable":true,"id_collides_with":0,"l":2734,"ord":0,"overloads":1,"amb":0,"cx":6,"ccx":5,"churn":20,"tested":0}, +{"p":"./src/testmap.h","n":"TestRow","scope":"TestRow","key":"7e95e329265d6e9a","id":"./src/testmap.h::TestRow::TestRow","id_addressable":true,"id_collides_with":0,"l":191,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":26,"tested":0}, +{"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1107,"ord":0,"overloads":1,"amb":0,"cx":5,"ccx":4,"churn":26,"tested":0}, +{"p":"./src/gitmine.h","n":"resolveSinceScope","scope":"rw","key":"9caaa3dbeaa688d0","id":"./src/gitmine.h::rw::resolveSinceScope","id_addressable":true,"id_collides_with":0,"l":302,"ord":0,"overloads":1,"amb":2,"cx":5,"ccx":4,"churn":35,"tested":0}, +{"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":345,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":16,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":618,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":320,"tested":0}], +"files":[{"p":"./src/crossref.h","symbols":1,"churn":27,"ccx":23,"hotspot_rank":23}, +{"p":"./src/darkflags.h","symbols":1,"churn":15,"ccx":7,"hotspot_rank":53}, +{"p":"./src/docdrift.h","symbols":2,"churn":20,"ccx":11,"hotspot_rank":24}, +{"p":"./src/gitmine.h","symbols":2,"churn":35,"ccx":6,"hotspot_rank":15}, +{"p":"./src/main.cpp","symbols":1,"churn":320,"ccx":1,"hotspot_rank":2}, +{"p":"./src/pageview.h","symbols":1,"churn":16,"ccx":2,"hotspot_rank":108}, +{"p":"./src/testmap.h","symbols":2,"churn":26,"ccx":4,"hotspot_rank":34}, +{"p":"./src/verbs_change.h","symbols":2,"churn":38,"ccx":77,"hotspot_rank":19}]},"blast_radius":{"reaches":47,"files_total":18,"capped":false,"files":["./src/crossref.h","./src/darkflags.h","./src/docdrift.h","./src/editplan.h","./src/flipimpact.h","./src/gitmine.h","./src/main.cpp","./src/mcp.h",". … [line truncated: 223 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":4,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":4,"max_ccx":69,"sum_ccx":131,"ambiguous_calls":41,"blast_reaches":47,"contract_touches":0,"conflicts":0,"untested":47," … [line truncated: 140 more bytes on this line] +{"id":"lane-1","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/recall.h","n":"recallHeadingSections","scope":"rw","key":"12c807e5f4b10399","id":"./src/recall.h::rw::recallHeadingSections","id_addressable":true,"id_collides_with":0,"l":1073 … [line truncated: 69 more bytes on this line] +{"p":"./src/main.cpp","n":"kJsonRideAlongFlags","scope":"","key":"2a90806fa5a580c1","id":null,"id_addressable":false,"id_collides_with":0,"l":2677,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":320,"tested":0}, +… [68 more display lines; full output is 19831 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +Input file: + +````` +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 +````` + +````` +{"v":1,"verb":"plan-lanes","at":"f8d431396","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2039,"symbols":18699,"edges":22330,"ambiguous":7928,"unresolved":4925},"carve":null,"core":{"files": … [line truncated: 3 more bytes on this line] +"symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] +{"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2279,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":20,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":544,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":192,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":2985,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":305,"tested":0}, +{"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":24,"tested":0}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2779,"ord":0,"overloads":1,"amb":12,"cx":12,"ccx":13,"churn":20,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2319,"ord":0,"overloads":1,"amb":13,"cx":22,"ccx":35,"churn":20,"tested":0}, +{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":278,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":20,"tested":0}, +{"p":"./src/darkflags.h","n":"FlagsResult","scope":"FlagsResult","key":"79dd3baf183323bf","id":"./src/darkflags.h::FlagsResult::FlagsResult","id_addressable":true,"id_collides_with":0,"l":837,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":15,"tested":0}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1691,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":38,"tested":0}, +{"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":119,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":18,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpRequiredFields","scope":"rw::mcprefuse","key":"c5f1ad5fc3a12368","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpRequiredFields","id_addressable":true,"id_collides_with":0,"l":66,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":32,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":1,"churn":305,"ccx":0,"hotspot_rank":6}, +{"p":"./src/darkflags.h","symbols":1,"churn":15,"ccx":0,"hotspot_rank":53}, +{"p":"./src/docdrift.h","symbols":5,"churn":20,"ccx":49,"hotspot_rank":24}, +{"p":"./src/ensemble.h","symbols":1,"churn":18,"ccx":0,"hotspot_rank":67}, +{"p":"./src/mcprefusal.h","symbols":1,"churn":32,"ccx":0,"hotspot_rank":39}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":192,"ccx":0,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":24,"ccx":4,"hotspot_rank":35}, +{"p":"./src/verbs_change.h","symbols":1,"churn":38,"ccx":4,"hotspot_rank":19}]},"blast_radius":{"reaches":12,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":12,"module_span":8,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":8,"max_ccx":35,"sum_ccx":57,"ambiguous_calls":27,"blast_reaches":12,"contract_touches":0,"conflicts":3,"untested":12,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":100,"l":255,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":4," … [line truncated: 11 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2610,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":305,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":2985,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":305,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":2689,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":305,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":100,"l":858,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":15,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":289,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":32,"tested":0}, +{"p":"./src/cli.h","n":"serveHelpSelector","scope":"rw","key":"4b247ee064fc4e94","id":"./src/cli.h::rw::serveHelpSelector","id_addressable":true,"id_collides_with":0,"l":2754,"ord":0,"overloads":1,"amb":2,"cx":4,"ccx":3,"churn":305,"tested":0}, +… [62 more display lines; full output is 18834 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes=99 --task=x` + +*Out-of-range refusal shape for the lane count.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out) +````` + +## `./build/ripwire . --layout=Symbol` + +*CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped enum's underlying type is not a byte layout) +````` + +## `./build/ripwire . --doc-drift` + +*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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [179 more display lines; full output is 24759 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --doc-drift --gateability` + +*The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [202 more display lines; full output is 26552 bytes on 1 raw line(s)] +````` + +Tail of the same output — the `` section: + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doc-drift --with-history` + +*Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [166 more display lines; full output is 25529 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --from-trace=-` + +*Map a pasted stack trace onto indexed symbols. CHANGED: in_corpus= now reports the real count (was 0).* + +Input file: + +````` +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:3406 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:3447 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1441 + #3 0x102e01a44 in main src/main.cpp:3475 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) +==41337==ABORTING +````` + +````` + + + + + + + + + + +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] + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) +int runDefaultMap( const MainDispatch& d ) +int main( int argc, char** argv ) + + + +& 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 ) + { + double teleportMass = 0.0; + for( const double value : teleport ) +… [17 more display lines; full output is 5666 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --notes` + +*List all field notes (write-side memory) — the committed .ripwire_notes at the repo root, each with the sha/branch it was recorded at.* + +````` + + + + + + + + + + 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.]]> + + + + +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI"` + +*ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* + +````` + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 104 more bytes on this line] + +knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedBy makes in full above, which applies here verbatim: firstFlagOutside() walks the rows parseArgs itself minline constexpr std::string_view kInRideAlong[] = + +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 ) noexcept +struct AgentTarget + +verifier N2/N3/N11: the bad-VALUE refusal tablestruct McpValueSpec + + + + + + + + + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + +… [49 more display lines; full output is 9584 bytes on 47 raw line(s)] +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` + +*Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* + +````` + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const std::vector<std::string>& verbLines… + +knows; these are all OUTSIDE that table. DERIVED, NOT ENUMERATED — the argument htmlPreemptedB…inline constexpr std::string_view kInRideAlong[] = + +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 ) noexcept + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + + + + + + + + +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 RankDisclosure + +What a rank call hands back: the vector, and the power iteration's own account of itself. Struct…struct RankedGraph + +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… +struct PageRankRun +COLD_FILES = ( # ── ingest sections that are not on the per-file / per-symbol default path ──────────────────── ( "s… +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort… +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> … [line truncated: 13 more bytes on this line] +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +## Background — read these, in this order +## Design space and constraints +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +struct ColdParseReserve +std::vector<std::uint32_t> denseIdOfRoot( symbolCount, UINT32_MAX ) +probe_args() +void finalizeCrawlSkips( CrawlSkips& skips, const HashMap<std::string, std::uint64_t>& extTally ) +## The arithmetic — derive it from the … [line truncated: 44 more bytes on this line] +… [56 more display lines; full output is 10317 bytes on 10 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +````` +(empty) +````` + +Artifact written: + +````` + 304618 /aux/ripwire2.cc.json +{"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri +````` + +## `./build/ripwire . --batch=/aux/batch2.txt` + +*One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* + +Input file: + +````` +for:incremental cache invalidation +callers:rankGraphTeleport +grep:DEGRADED_PATH_ALERT +lego:Vehicle +````` + +````` + + + + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) + +Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] + +P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) +struct FileHealth +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vec… +inline const McpIndex& getIndex( const std::string& root ) +struct McpIndex +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +### See the map — not just the numbers +inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& cacheStats … [line truncated: 59 more bytes on this line] +IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool respectGi … [line truncated: 13 more bytes on this line] +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) +static bool cachePathIsDirectory( const std::string& cachePath ) +inline CacheFrame openCacheFrame( const std::string& path, bool captureValueUses ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +… [67 more display lines; full output is 26788 bytes on 1 raw line(s)] +````` + + +--- + +# self-diagnosis + +## `./build/ripwire . --doctor` + +*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).* + +**exit code: 1** + +````` + + + + + + + + + +````` + + +--- + +# security + +## `./build/ripwire --scan-skill=skills/ripwire-orient/SKILL.md` + +*Scan a single skill file for injection/exfiltration patterns before installing.* + +````` + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) in skills/ripwire-orient/SKILL.md +````` + +## `./build/ripwire --scan-skills=skills` + +*Scan a whole skills directory (exit 2 = CRITICAL, 1 = WARN). Explicit-DIR form only.* + +````` + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s) skipped, 0 denylisted subtree(s) not descended) +````` + + +--- + +# knobs / modes + +## `./build/ripwire . --rank-by=churn --top-k=5` + +*Rank by git change-frequency prior instead of PageRank.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~7 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=bogus --top-k=5` + +*An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf|churn|churn-decay) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=columnar` + +*Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* + +````` + + + + +0=src/eval.h 1=src/graph.h 2=src/main.cpp 3=src/mcpindex.h + +0,1,1,2,2,2,3 +runEval,rankGraph,anchoredLexicalRank,churnDecayRanking,churnRankedGraph,runDefaultMap,getIndex +171,3445,3995,1217,1252,1439,1108 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +````` + + + +inline std::string spanTierMemoPath( const std::string& diskPath ) + + +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) + + +### See the map — not just the numbers + + +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) + + +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=bogus` + +*An unknown --format value REFUSES (exit 1), named, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidates) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --json` + +*Machine-parseable JSON, same content, keys mirror the XML attrs.* + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"callers":[{"t":"fn","n":"runEval","p":"src/eval.h:171"}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:3445"}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:3995"}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1217"}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1252"}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1439"}, +{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1108"}]} +````` + +## `./build/ripwire . --hotspots --json` + +*JSON refusal shape: an unsupported verb refuses loudly instead of silently falling back to XML.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --json is not yet supported for --hotspots — supported: the default map, --for, --pack-task, --callers/--callees, --impact, --quality-delta, --test-gate, --metrics, and --plan-lanes which is JSON-native (e.g. ripwire --callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +````` + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3` + +*Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* + +````` + + + + + + + + + + + + + + + + +````` + +Artifact written: + +````` + 12415045 /aux/warm2.ripwirecache +````` + +## `./build/ripwire . --max-file-size=8K --top-k=3` + +*Skip files above a size bound before parsing (note the corpus shrink in the header).* + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --scip=does_not_exist.scip --callers=rankGraphTeleport` + +*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).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather than serving the name-based map you named a precision index to improve on (generate one with scip-clang/scip-python, or drop --scip) +````` + +## `./build/ripwire src test --top-k=5` + +*Multi-root workspace: ONE merged graph over two roots, paths labeled /.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 7.33s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 0.5% 1.5% 6.7% + BM25 10.9% 13.1% 16.7% + BM25sub 10.7% 15.1% 20.2% + BM25body 22.3% 29.9% 34.9% + fused 4.2% 14.8% 22.0% + anchored 20.8% 29.9% 34.9% + same-dir 1.6% 5.6% 7.2% + random 0.2% 0.5% 1.0% <- floor (random ranking over F=2039 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 + expansion (--for --anchor, EXPERIMENTAL). The SHIPPED default for --for/--query is + the subtoken+body lexical family (routed to name-exact only for an identifier-shaped + query — lexical.h chooseForRanker), so a gap between the ripwire and BM25* rows here + is structural-importance-vs-lexical-relatedness on a co-change task, not the shipped + retrieval path losing to an alternative it was never running. +````` + +## `./build/ripwire . --eval-retrieval` + +*Known-item retrieval eval: MRR + recall@k per ranker per query mode.* + +**wall time: 53.65s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=4164 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population + (smallest fnv1a64(scope::name) over the population, cut on the key so an identity is never split; + path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) + 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.714 57.9% 88.4% 92.9% + subtoken doc-phrase 0.932 91.2% 95.2% 95.8% + name-exact name 0.924 86.0% 97.4% 98.4% + name-exact doc-phrase 0.013 0.4% 2.2% 2.7% + anchored name 0.718 59.3% 87.4% 91.7% + anchored doc-phrase 0.927 90.5% 95.0% 95.8% + routed name 0.924 86.0% 97.4% 98.4% + routed doc-phrase 0.930 91.1% 95.0% 95.5% + note: routing chose name-exact on 3997/4000 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 + (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — + a common name (many definitions, or a subtoken carried by many symbol names) declines the route — so + conceptual prose falls back to subtoken+body; routed tracks the better ranker on BOTH modes + (routed==name-exact on name, ~=subtoken+body on doc-phrase). +````` + +## `./build/ripwire . --eval-stray=/aux/stray_labels2.tsv` + +*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.* + +**exit code: 1** + +Input file: + +````` +# 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) +no-such-ref-0 merged +no-such-ref-1 unmerged +no-such-ref-2 merged +````` + +````` +(empty) +````` + +stderr: + +````` +ripwire: --eval-stray: 3 labelled ref(s) do not exist in . -- not merged, just absent: no-such-ref-0, no-such-ref-1, no-such-ref-2 (fix the labels file or add the ref) +````` + +## `./build/ripwire skills --eval-skills=/aux/skills_labels2.tsv` + +*Labelled skill-ROUTING eval over the repo's own skills/ directory (4 hand-labelled prompts).* + +Input file: + +````` +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 +````` + +````` +ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus '/aux/skills_labels2.tsv'; split test=4 dev=0) + arm hit@1 hit@2 mrr sep-auc fire/abstain@ORACLE-th (upper bound) + overlap 66.7% 66.7% 0.690 0.000 50.0% (th=-1.000) + name 33.3% 66.7% 0.537 0.667 50.0% (th=0.000) + bm25-desc 66.7% 66.7% 0.690 0.667 75.0% (th=2.958) + bm25-full 33.3% 66.7% 0.611 1.000 50.0% (th=0.426) + for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.586) + random 6.2% 12.5% 0.211 0.500 <- floor (uniform-random ranking; auc 0.5 by definition) + provenance hit@1 (bm25-desc): router 0/0, desc 0/0, judged 2/3 (desc rows quote the descriptions - expect them easiest; judged is the honest number) + judged-only hit@1 per arm: overlap 2/3, name 1/3, bm25-desc 2/3, bm25-full 1/3, for-routed 1/3 + router-magnet: with ripwire-router ADMITTED as a candidate it takes top-1 on 1/3 positive prompts (bm25-desc arm) - why it is excluded above + per-skill (bm25-desc): name / permitted-rows / won / pos-fires / false-fires / neg-fires + ripwire-before-you-build 0 0 1 1 0 + ripwire-change-check 1 0 0 0 0 + ripwire-find-bug 0 0 0 0 0 + ripwire-fresh-eyes 0 0 0 0 1 + ripwire-graph-query 0 0 0 0 0 + ripwire-handoff 0 0 0 0 0 + ripwire-layers 0 0 0 0 0 + ripwire-mcp 0 0 0 0 0 + ripwire-navigate 1 1 1 0 0 + ripwire-opt-remarks 0 0 0 0 0 + ripwire-orient 1 1 1 0 0 + ripwire-perf-target 0 0 0 0 0 + ripwire-quality-bar 0 0 0 0 0 + ripwire-reuse-first 0 0 0 0 0 + ripwire-security-scan 0 0 0 0 0 + ripwire-write-tests 0 0 0 0 0 + misses (overlap): + line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" +… [19 more lines, 3665 bytes total] +````` + +## `./build/ripwire wrap claude` + +*Print the recipe to wire ripwire into Claude Code as an MCP server.* + +````` +# ripwire -> Claude Code (CLI-first) +# RECOMMENDED — this agent can run shell commands, so call the CLI directly. It costs +# nothing until you invoke it, and it reads CLAUDE.md, so the paste block below IS the wiring: +ripwire . --for="" --token-budget=2000 +# +# ...then add --legend=compact to every FOLLOW-UP call: the legend is a small share of a --for +# bundle but most of a --callers/--uses/--impact answer, and the payload is byte-identical either +# way. `ripwire --help` carries the measured range (one place, gate-held) -- this line does not +# repeat it, because two copies of a number is one copy that goes stale: +# ripwire . --callers=SYM --legend=compact +# +# ALTERNATIVE — register the MCP server instead, for a warm index across calls: +claude mcp add ripwire -- ripwire --mcp +# verbs the agent can then call mid-task (31 total): +# read: analyze, find_symbol, find_referencing_symbols, grep, cochange, memory_recall, situational_awareness, mentions, for, lego, owners, fetch_body, batch, flags, doc_drift, slice +# flagship reflex: exemplar, quality_delta, quality_baseline, impact, uses, path_between, connect, explore, from_trace, edit_check, whereis, stray_content +# edit: replace_symbol_body, insert_before_symbol, insert_after_symbol +bash skills/install.sh # deploy to ${CLAUDE_CONFIG_DIR:-~/.claude}/skills (drift-gated) +bash skills/install.sh --hook # RECOMMENDED: advisory Read/Grep -> ripwire CLI nudge + session primer (opt-in, never blocks) +# +# context wiring — a binary on PATH is invisible to an agent until its rules file says when +# to reach for it. Paste the block below into CLAUDE.md: +# --- paste into CLAUDE.md --- +## ripwire — deterministic codebase maps (on PATH as `ripwire`) +Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after that warm, ~0.1s. +- Orient on a task: `ripwire --for=""` — ranked, quality-annotated + signatures. Paste symbol/file names from the issue verbatim; named mentions get anchored. +- One task: `--pack-task="" --legend=compact`; before parallel agents: `--plan-lanes=N --task=""`, then read `lanes[].execution`. +- Have a stack trace / build error: `ripwire --from-trace=FILE --legend=compact` (`-` = stdin) — + paste the error, don't paraphrase it into a query. +… [14 more lines, 4027 bytes total] +````` + +## `./build/ripwire --version` + +*Version + short build info.* + +````` +ripwire 0.6.1 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=f8d431396) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:3406` + +*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 . --callers=@src/graph.h:3406` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --at=src/graph.h:999999` + +*A seed past the end of the file — the refusal shape for a faulted location.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 6763 lines — the seed asked for line 999999) +````` + +## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` + +*VERIFY a closed claim in one call: three-valued verdict (confirmed / refuted / not-established) with the evidence rows inline.* + +````` + + + + + + + +````` + +## `./build/ripwire . --verify="unused(rankGraphTeleport)"` + +*A claim that is FALSE — the refuted shape, with the references that refute it.* + +````` + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --verify="contains(src/graph.h, \"no such literal anywhere\")"` + +*A literal-scan absence: refuted only with complete= evidence, never on a partial scan.* + +````` + + + + + +````` + +## `./build/ripwire . --verify="frobnicate(x)"` + +*An unparseable claim — the refusal names the accepted shapes.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. The claim language is CLOSED; the shapes are: calls(A, B) does A transitively call B · uses(SYM) is SYM referenced anywhere · unused(SYM) is SYM referenced nowhere · contains(FILE, "LITERAL") do FILE's bytes contain t … [line truncated: 218 more bytes on this line] +````` + +## `./build/ripwire . --slice=rankGraphTeleport` + +*Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* + +````` + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport` + +*Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* + +````` + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3` + +*TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* + +````` + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + + pw = biasPrior( g, p );]]> + + +& p, float alpha = 0.85f )]]> + + +& p, float alpha = 0.85f )]]> + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd` + +*Forward flow: which statements the seed's value reaches, at the default depth bound.* + +````` + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + + + + + + + + + + + + + + 0.0 )]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:nosuchvar` + +*A variable the definition does not bind — the refusal shape, naming the inventory.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice: no occurrence of 'nosuchvar' in rankGraphTeleport — sliceable locals: alpha, g, p, pw, N, teleport, rankDouble, run, teleportMass, value, inverseMass, r (bare --slice=rankGraphTeleport lists them with first-def lines) +````` + +## `./build/ripwire . --slice-depth=3` + +*--slice-depth without --slice-flow is refused loudly rather than silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire --slice=parseArgs:argIndex --slice-flow=fwd --slice-depth=4) +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --legend=compact` + +*The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* + +````` + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + +````` + +## `./build/ripwire . --pattern='rankGraphTeleport($A, $B, $C)'` + +*Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* + +````` + +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) + +````` + +## `./build/ripwire . --pattern='DEGRADED_PATH_ALERT(...)'` + +*The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* + +````` + +DEGRADED_PATH_ALERT( "abi: no merge-base for a ref (unrelated history?) — that ref is counted, not compared" ) +DEGRADED_PATH_ALERT( "arch: malformed rules line — rules file rejected" ) +DEGRADED_PATH_ALERT( "arch: refusing to read the arch baseline sidecar through a symlink" ) +DEGRADED_PATH_ALERT( "arch: refusing to write the arch baseline sidecar through a symlink" ) +DEGRADED_PATH_ALERT( "atoms: an exclusion capture stream spent its whole budget; the rules reading it are suppressed thi +DEGRADED_PATH_ALERT( "clones: Type-3 pair cap hit — first N compared (both-gate-surviving) near-misses kept, rest skip +DEGRADED_PATH_ALERT( "comment-coherence: an indexed file could not be read — its functions are absent from the report" +DEGRADED_PATH_ALERT( "crossref: cannot write the blob-batch list — cross-branch content unavailable" ) +DEGRADED_PATH_ALERT( "crossref: git cat-file --batch failed to start — cross-branch content unavailable" ) +DEGRADED_PATH_ALERT( "crossref: git cat-file stream ended mid-blob — stopping the batch rather than risk misattributin +DEGRADED_PATH_ALERT( "crossref: for-each-ref yielded a ref whose tip is not an object name — skipping it" ) +DEGRADED_PATH_ALERT( "crossref: refusing a diff whose revision arguments are not resolved object names" ) +DEGRADED_PATH_ALERT( "crossref: a git worker threw — this shard of the sweep is incomplete" ) +DEGRADED_PATH_ALERT( "crossref: ref tip or HEAD is not a resolved object name — refusing to probe, verdict is unknown" +DEGRADED_PATH_ALERT( "crossref: merge-base returned something that is not an object name — discarding it" ) +DEGRADED_PATH_ALERT( "crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not +DEGRADED_PATH_ALERT( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ) +DEGRADED_PATH_ALERT( "whereis: the index's def sites match no HEAD row (working tree drifted from HEAD?) — keeping the +DEGRADED_PATH_ALERT( "flags: cannot walk root for CMake files — cmake gates omitted" ) +DEGRADED_PATH_ALERT( "flags: a CMake file's symlink target leaves the root — file refused" ) +DEGRADED_PATH_ALERT( "dmm: a materialized commit tree ingested empty" ) +DEGRADED_PATH_ALERT( "doc-drift: cannot walk the root — the on-disk existence probe is skipped" ) +DEGRADED_PATH_ALERT( "doc-drift: a file's symlink target leaves the root — file refused" ) +DEGRADED_PATH_ALERT( "doc-drift: cannot read a markdown file — its anchors are omitted" ) +DEGRADED_PATH_ALERT( "doc-drift gateability: live total disagrees with drift= — projected_drift clamped to a floor of +DEGRADED_PATH_ALERT( "docparse: popen failed for markitdown bridge" ) +DEGRADED_PATH_ALERT( "docparse: cannot read document file" ) +DEGRADED_PATH_ALERT( "edit-check: no git HEAD baseline — status no-baseline" ) +… [73 more display lines; full output is 18085 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --pattern='x'` + +*A pattern that collapses to a bare token is REFUSED — never reported as hits=0.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --pattern: this pattern collapses to a single bare token, which is a TEXT search, not a structural one — a clean parse is not enough (a bare word parses fine in most grammars and means nothing structural). Give it a shape (foo($X), $A + $B, if ($C) { ... }) or use the grep flag for litera … [line truncated: 187 more bytes on this line] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache` + +*Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [246 more display lines; full output is 19947 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file` + +*Drop every hit in a file that ALSO contains the --not literal anywhere (file scope).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [173 more display lines; full output is 15682 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep=cache` + +*A second --grep= REFUSES and names --and= as the AND spelling — no silent overwrite.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --grep given twice; did you mean --grep='A' --and='B'? +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-in=any` + +*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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [548 more display lines; full output is 31907 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=deterministic` + +*A literal whose classified hits are all prose: the answer serves tier="comment+string" rather than an empty code tier, and tier_unclassified= says how many hits the fixed parse budget never classified.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [692 more display lines; full output is 45223 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --handles` + +*h= on each editable enclosing-symbol row: a freshness-pinned identity an edit verb can target and must refuse on after any file change.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + += kType3MaxPairs ) { DEGRADED_PATH_ALERT( "clones: Type-3 pair cap hit — first N compared (both-gate-surviving) near-misses kept, rest skipped" ); goto done; }]]> + + + +… [566 more display lines; full output is 38511 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --grep=DEGRADED_PATH_ALERT --legend=compact` + +*The grep compact legend (ripwire.grep/v1).* + +````` + + + + +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 * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() + +errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 48> kLangTable = +inline bool refuseKotlinNesting( const LangEntry& le, std::string_view bytes, const char* path, std::size_t fileId, IngestFileScan& scan ) +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline SymTreeIndex indexCommittish( const std::string& root, const std::string& committish, const std::vector<std::string>& excludes, std::size_t maxFileBytes,…std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& … [line truncated: 8 more bytes on this line] +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +struct LintRule +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [56 more display lines; full output is 9285 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` + +*Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* + +````` + + + + + +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 * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() + +errNodes/errBytes are a PARSER-STATE fact, never a syntax verdict: tree-sitter error recovery fi…struct FileHealth +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 48> kLangTable = +inline bool refuseKotlinNesting( const LangEntry& le, std::string_view bytes, const char* path, std::size_t fileId, IngestFileScan& scan ) +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline SymTreeIndex indexCommittish( const std::string& root, const std::string& committish, const std::vector<std::string>& excludes, std::size_t maxFileBytes,…std::vector<std::vector<rw::AstMatch>> builtInLintCaptures( const rw::IngestResult& ing, const std::vector<rw::AstQuerySpec>& checks, std::vector<std::string>& … [line truncated: 8 more bytes on this line] +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +struct LintRule +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [94 more display lines; full output is 13207 bytes on 47 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` + +*Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* + +````` + + + + +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 +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +inline const char* kAcksFile = ".ripwire_quality_acks" +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which de … [line truncated: 76 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +struct QualityDeltaLegendParts +inline constexpr const char* kScopeLegend = "SCOPE, present only when the scope flag was given, and it NARROWS WHAT THIS REPORT " "CLAIMS: scope= is the pattern list it was g … [line truncated: 83 more bytes on this line] +… [52 more display lines; full output is 9988 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` + +*The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* + +````` + + + + +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 +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… +inline const char* kAcksFile = ".ripwire_quality_acks" +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string renderAckRecords( const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string qualityAcksPath( const std::string& root ) +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline std::string gitHeadSha( const std::string& root ) +int runQualityBaselinePin( const MainDispatch& d, const std::string& baselineFile, const std::string& acksFile ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes ) +inline constexpr const char* kQdSchemeLegend = "A THIRD re-filing, git-independent: on 2026-08-25 the per-symbol quality key stopped being a " "canonical-id hash (which de … [line truncated: 76 more bytes on this line] +inline std::string qualityBaselinePath( const std::string& root ) +struct QualityDeltaLegendParts +inline constexpr const char* kScopeLegend = "SCOPE, present only when the scope flag was given, and it NARROWS WHAT THIS REPORT " "CLAIMS: scope= is the pattern list it was g … [line truncated: 83 more bytes on this line] +… [53 more display lines; full output is 10094 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --safe-delete=rankGraphTeleport` + +*"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --safe-delete=DoesNotExist` + +*Unknown-symbol refusal shape for --safe-delete.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --safe-delete symbol not found: DoesNotExist +````` + +## `./build/ripwire . --handoff` + +*The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --handoff --token-budget=1200` + +*The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --skipped` + +*WHY a file is not in the index (oversize / excluded / unsupported-ext / gitignored) and which indexed files it cannot vouch for (degraded-parse, minified-suspect), plus the per-language census.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [358 more display lines; full output is 41954 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --no-ignore --top-k=3` + +*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-stable --top-k=3` + +*--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of.* + +````` + + + + + + + + + + + + + + + + +````` + +stderr: + +````` +ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable ordering the server turns on) — it changed nothing here; the CLI map orders important-first unless you pass --order=stable +````` + +## `./build/ripwire . --run-trace="cat /aux/asan_trace_now.txt; exit 1"` + +*EXEC-MODE --from-trace: run a command, and on a non-zero exit map its captured output onto indexed symbols in the same call — the whole fix-loop entry. ripwire exits 4 here because the wrapped command failed, which is the signal, not an incident.* + +**exit code: 4** + +````` + + + + + + const&, float) src/graph.h:3406 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:3447 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1441 + #3 0x102e01a44 in main src/main.cpp:3475 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> 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 ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { + const double inverseMass = 1.0 / teleportMass; + for( double& value : teleport ) + { + value *= inverseMass; + } + } + run = pageRankDouble( g.inEdges, g.wOutDeg, teleport, rankDouble, PageRankConfig{ .alpha = double( alpha ) } ); + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESCR … [line truncated: 535 more bytes on this line] +````` + +## `./build/ripwire . --run-trace="true"` + +*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="sleep 30" --run-timeout=2` + +*A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* + +**exit code: 4** — **wall time: 2.11s** + +````` + + + + +````` + +stderr: + +````` +ripwire: --run-trace: TIMEOUT — the command exceeded the 2 s cap; its process group was killed +````` + +## `./build/ripwire . --run-timeout=5` + +*--run-timeout alone is refused loudly (it only modifies --run-trace).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwire --run-trace="make -j" --run-timeout=60) +````` + + +--- + +# assess quality — the wider lens family + +## `./build/ripwire . --quality-panel` + +*THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* + +**wall time: 1.71s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [188 more display lines; full output is 18135 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --readability --limit=8` + +*Per-function readability, LEAST readable first (Halstead volume, token entropy, lines, Posnett) — a RANKING lens, not a grade.* + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --comment-coherence --limit=8` + +*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 . --context-ratio --limit=8` + +*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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [22 more display lines; full output is 13302 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --nonlocal-state --limit=8` + +*Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [49 more display lines; full output is 10623 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [37 more display lines; full output is 10936 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity` + +*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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [497 more display lines; full output is 44384 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity=Symbol` + +*The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --naming-consistency --limit=8` + +*The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [12 more display lines; full output is 5436 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --naming-calibration` + +*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.* + +**wall time: 3.67s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [61 more display lines; full output is 9200 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --naming-locals` + +*The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [635 more display lines; full output is 73779 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-catalog` + +*The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed.* + +````` + + +a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_cast +a goto statement — cppcoreguidelines-avoid-goto +a 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-parentheses +an if-body that is just `;` — bugprone-suspicious-semicolon +a C-style typedef struct/union where `using` is preferred +a non-trivial numeric literal inside a function body, outside a const/constexpr init +a catch block with an empty body +x = x — almost always a copy-paste bug +a function body over 80 lines +brace nesting depth over 4 inside a function body +a bare `return;` mixed with a value-returning return in the same function +a statement after an unconditional return/break/continue/throw/raise in the same block +a 1-2 letter Function/Method/Var name — visible far beyond any tiny scope [Beniamini/Hofmeister] +more than 5 split tokens in one name [Butler; AlSuhaibani] +foo1/foo2/... digit-suffix siblings sharing a base name in one scope [Butler] +internal consecutive underscores, or a C-family reserved __x/_X form [Butler] +snake_case and camelCase mixed inside one name [Butler] +an is/has/can/should/was-prefixed name whose KNOWN return type is not bool-like [LAPD A2] +a set-prefixed name whose KNOWN return type is not void-like [LAPD A3] +a co-visible pair within edit distance <=2, reordered tokens, or a bare/digit-suffixed twin [Namesake] +every split subtoken is corpus-ubiquitous (BM25 idf) on a body past the size floor — fires only at the low end [Sparck Jones 1972] +the comma operator inside an expression (never a for-header comma) [Gopstein FSE 2017] +++/-- evaluated inside a larger expression (never a whole statement) [Gopstein FSE 2017] +an assignment whose VALUE is consumed (a condition, an argument, ...) [Gopstein FSE 2017] +a conditional expression inside a conditional expression [Gopstein FSE 2017] +… [12 more display lines; full output is 8031 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=cache-` + +*Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* + +````` + + + + + + + + + + +vector<std::vector<double>> +nodes[ perm[i] ] +nodes[ perm[ i + 1 ] ] +p = p->next +map +byName[ symN[i] ] +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +vector<std::vector<double>> +m[ in.keys[ i ] ] +map +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +map +m[ g_pool[ g_keyOf[i] ] ] +g_pool[ g_readOf[i] ] +g_pool[ g_readOf[i] ] +m[ g_pool[i] ] +… [424 more display lines; full output is 53797 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-ignore=naming-,cache-` + +*DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [724 more display lines; full output is 67006 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-'?) — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --lint-select=nosuchfamily` + +*A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --sarif` + +*The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* + +````` +{"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}}, +{"id":"self-assign","shortDescription":{"text":"self-assign"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"large-function","shortDescription":{"text":"large-function"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"deep-nesting","shortDescription":{"text":"deep-nesting"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"inconsistent-return","shortDescription":{"text":"inconsistent-return"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"unreachable-code","shortDescription":{"text":"unreachable-code"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-short","shortDescription":{"text":"naming-short"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-wordy","shortDescription":{"text":"naming-wordy"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-series","shortDescription":{"text":"naming-series"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-underscore","shortDescription":{"text":"naming-underscore"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-case","shortDescription":{"text":"naming-case"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-predicate","shortDescription":{"text":"naming-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-setter","shortDescription":{"text":"naming-setter"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-confusable","shortDescription":{"text":"naming-confusable"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-uninformative","shortDescription":{"text":"naming-uninformative"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-comma-operator","shortDescription":{"text":"atom-comma-operator"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-embedded-crement","shortDescription":{"text":"atom-embedded-crement"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-assign-as-value","shortDescription":{"text":"atom-assign-as-value"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-nested-ternary","shortDescription":{"text":"atom-nested-ternary"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-implicit-predicate","shortDescription":{"text":"atom-implicit-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-octal-literal","shortDescription":{"text":"atom-octal-literal"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +… [8815 more display lines; full output is 1225707 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 4403 +````` + +## `./build/ripwire . --lint --sarif --limit=5` + +*SARIF is always the FULL result set: paging alongside it refuses loudly.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M +````` + +## `./build/ripwire . --dmm` + +*The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD` + +*The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* + +**wall time: 2.18s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD~3..HEAD` + +*The range form: tree HEAD vs tree HEAD~3.* + +**wall time: 2.18s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --cochange --cochange-groups` + +*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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [437 more display lines; full output is 30224 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange --cochange-recur=2` + +*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 . --html=/aux/map2.html --color-by=community` + +*The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* + +````` +(empty) +````` + +Artifact written: + +````` + 158951 /aux/map2.html +````` + +## `./build/ripwire . --index-out=/aux/ci_index` + +*CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (12415045 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (28682761 bytes, rich family) +````` + +Artifact written: + +````` + 12415045 /aux/ci_index.lean.ripwirecache + 28682761 /aux/ci_index.rich.ripwirecache + 41097806 total +````` + +## `./build/ripwire . --cache=/aux/ci_index.lean.ripwirecache --top-k=3` + +*Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* + +````` + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3` + +*Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* + +````` + + + + + + + + + + + + + + + + +````` + +Artifact written: + +````` +# ripwire pin-census v2 C=kind\tmech\tpre\tpost\tflags\tcaller_id\tcallee\ttargets(|-sep)\tline +# line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): +# the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. +# O rows (only under --scip) are the SCIP oracle: O\tcaller_id\tcallee\ttargets(|-sep) +# a target of @external (a builtin / another package) or @nondef (an in-index parameter, local or +# attribute ripwire extracts no symbol for) means SCIP resolved the site to something that is NOT a +# ripwire definition — the index spoke, and disagrees with every in-repo target the C row names. +# S rows (v2) are the DEFINITION universe, one per symbol: S\tid\tkind\tline — the def side of the + 51796 /aux/pin_census.tsv +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` + +*The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates.* + +**exit code: 2** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave_ledger.md` + +*The ledger-shaped fixture through the same check (exit 2: one gating card).* + +**exit code: 2** + +````` + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=claude` + +*--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* + +**exit code: 1** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=nosuch` + +*Other --agent values refuse.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: unsupported --agent value 'nosuch' (supported: codex, claude) +````` + + +--- + +# the dirty-tree verbs (throwaway clone, NOT the read-only repo) + +Everything below runs with `cwd` = the throwaway clone at `/dirty` (`git clone --local` of this repo, then one deliberate regression in `src/infra/sortutil.h`). The read-only repo is never touched. The binary is the same `build/ripwire`, addressed absolutely. + +## `./build/ripwire . --situ` + +*Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* + +**wall time: 1.72s** + +````` +ripwire situational-awareness — 1 changed file(s), 14 symbols in them +root: . +at: f8d431396+dirty + [1] blast radius: 106 symbols across 30 files transitively depend on these changes (showing 8 of 30 files — shown=8 total=30 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=30) + counts_floor=1 graph_ambiguous=7931 graph_unresolved=4925 graph_unindexed=219 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missing; … [line truncated: 44 more bytes on this line] + src/mcpverbs.h (30 dependent symbols) + src/serialize.h (9 dependent symbols) + src/main.cpp (7 dependent symbols) + src/verbs_quality.h (6 dependent symbols) + bench/bench_sort_large.cpp (5 dependent symbols) + src/verbs_for.h (5 dependent symbols) + src/editplan.h (4 dependent symbols) + bench/bench_radix_ab.cpp (3 dependent symbols) + [2] tests to run (4) order=evidence: [changed] you edited it, [partner] named after a changed file, then hops asc (1 = direct); "(n): a, b" = n runner-less files sharing that evidence; a (run: …) is relative to root: + test/verify_radix.cpp [hops=1] (run: bash test/greptiercheck.sh) + test/adaptivecutshapefix/adaptive_cut_shape_test.cpp [hops=2] (run: bash test/adaptivecutshapecheck.sh) + test/includeprecise_unit.cpp [hops=2] (run: bash test/includeprecisecheck.sh) + test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) + script_gates_unmodelled=675 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="2502": + (none — 2502 commits were mined and none co-edited a file outside your diff) + next: --test-gate +````` + +## `./build/ripwire . --test-gate` + +*The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* + +**exit code: 4** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --quality-delta` + +*Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* + +**exit code: 2** — **wall time: 5.33s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 16026 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --legend=compact` + +*The same gating report under --legend=compact — same rows, same exit 2, schema="ripwire.quality-delta/v1", ~4 KB of legend down to one comment: the shape an agent's edit loop should run.* + +**exit code: 2** — **wall time: 2.36s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 10165 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --json` + +*The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* + +**exit code: 2** — **wall time: 2.35s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":83,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":24,"api-new-surface":2,"at":"f8d431396+dirty","renames":57,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated":1 … [line truncated: 186 more bytes on this line] +{"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"duplication","members":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","tokens":59,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"nesting","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":6,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"new-clone-of-reused-helper","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","was":0,"now":4,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"params","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","was":0,"now":8,"p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"verbosity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":8,"now":47,"p":"src/infra/sortutil.h:49","sev":"minor"}], +"sa":[{"kind":"api-surface","key":"298e798c7f075715","why":"target-gone"}, +{"kind":"api-surface","key":"5a07390012b46e06","why":"target-gone"}, +{"kind":"api-surface","key":"7f2c3eefdf6e512e","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1926b0d9e94541a0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1d3814ba687a4aef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1db76028879244ef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"995375dfa4e63104","why":"target-gone"}, +{"kind":"complexity","key":"4b309450f25c2b44","why":"finding-gone","sym":"src/ingest.cpp::rw::ingest","p":"src/ingest.cpp:220"}, +{"kind":"complexity","key":"53a1214d5505fa41","why":"finding-gone","sym":"main","p":"src/main.cpp:3473"}, +{"kind":"complexity","key":"7a04eee0ff6ec2d7","why":"finding-gone","sym":"src/recall.h::rw::buildSectionGranularBody","p":"src/recall.h:1223"}, +{"kind":"dead-code:new-symbol","key":"033294a4fc57eac8","why":"finding-gone","sym":"test/timsort_harness.cpp::Rec::operator==","p":"test/timsort_harness.cpp:93"}, +{"kind":"dead-code:new-symbol","key":"0a892722cd821466","why":"finding-gone","sym":"operator new[]","p":"test/timsort_harness.cpp:71"}, +{"kind":"dead-code:new-symbol","key":"0f873ba546cd729f","why":"finding-gone","sym":"test/timsort_harness.cpp::ByKey::operator()","p":"test/timsort_harness.cpp:98"}, +{"kind":"dead-code:new-symbol","key":"23656ac3555b293c","why":"finding-gone","sym":"test/timsort_harness.cpp::Rec::Rec","p":"test/timsort_harness.cpp:87"}, +{"kind":"dead-code:new-symbol","key":"7ea8c2536c103f65","why":"finding-gone","sym":"Shape","p":"test/timsort_harness.cpp:105"}, +{"kind":"dead-code:new-symbol","key":"86cc58659800f0fb","why":"finding-gone","sym":"operator delete","p":"test/timsort_harness.cpp:76"}, +{"kind":"dead-code:new-symbol","key":"aba70b100ec42e27","why":"finding-gone","sym":"operator delete[]","p":"test/timsort_harness.cpp:77"}, +… [62 more display lines; full output is 10050 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope` + +*NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* + +**exit code: 1** — **wall time: 1.55s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing written +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=api-surface` + +*NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* + +**wall time: 1.55s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already acked) → ./.ripwire_quality_acks +````` + +## `./build/ripwire . --quality-delta` + +*Re-run after the partial ack: acked=3, the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 2.32s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [64 more display lines; full output is 16313 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 4 preexisting-worse major finding(s); first: complexity src/infra/sortutil.h::rw::sortutil::lessByScoreDescId at src/infra/sortutil.h:49 (was=1 now=67) +````` + +## `./build/ripwire . --ack-only=gating` + +*--ack-only WITHOUT --quality-ack REFUSES loudly (exit 1, the pairing named) — it used to be silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire

--quality-delta --ack-only=contract-change --quality-ack="reason") +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey` + +*A real contract-change: was=1 now=2 params, with the call sites that are now provably incompatible, and a pasteable next= (--uses=SYM) on the root.* + +````` + + + + + + +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=compact` + +*The same verdict under --legend=compact: ~5.7 KB of legend becomes one comment, every row identical — the post-edit reflex at its cheapest.* + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [79 more display lines; full output is 11620 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*The map re-ranked with a teleport toward the changed file (changed=1 here, not 0).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [34 more display lines; full output is 4801 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [311 more display lines; full output is 18667 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zz-orphan` + +*CHANGED: a ref with NO merge base with HEAD now reports v="unknown" ok="0" in its own bucket — the absence of an answer, never a claim it is merged. (The sandbox carries a deliberately parentless branch built with `git commit-tree`; a shallow CI clone puts every ref here.)* + +````` + + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1055, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --stray-content=zz-orphan --plan` + +*CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* + +````` + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1055, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --dmm` + +*The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* + +````` + + +

+

+ +````` + +## `./build/ripwire . --quality-delta --scope=src/graph.h` + +*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.* + +**wall time: 2.39s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 17306 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --quality-ack --scope=src/graph.h --ack-only=api-surface` + +*The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* + +**exit code: 1** — **wall time: 1.56s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=src/graph.h — refusing, and writing nothing at all: + api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 + those rows belong to whoever is editing those paths. Acking them here writes their debt into a committed ledger under YOUR + reason string, which is how a per-finding ratchet becomes a rubber stamp. Narrow the pattern, or widen the scope if they really are yours. +````` + +## `./build/ripwire . --handoff` + +*The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --note-add="lessByScoreDescId: keep this branch-free — it sits inside the PageRank sort comparator"` + +*Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-09-14 keep this branch-free — it sits inside the PageRank sort comparator f8d4313969bf8d93ec05dd1e122d3d7c679b6643 HEAD +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sortutil.h::rw::sortutil::lessByScoreDescId' — that is the id --for/--expand key notes by +````` + +## `./build/ripwire . --notes` + +*The note is listed under the canonical id, dangling="0" — i.e. it will actually surface. (Before the H1 fix the bare name was stored verbatim and read dangling="1": recorded, and surfaced nowhere.)* + +````` + + + + + + + + + + + + + + + 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.]]> + + + + +````` + +## `./build/ripwire . --note-add="gitOneLine: which one?"` + +*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.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'gitOneLine' is ambiguous — it matches 2 definitions in 2 distinct contracts, and a note keys ONE canonical id (it would surface on one of them and look absent on the rest). Qualify one: ./src/handoff.h:gitOneLine, ./src/quality.h:gitOneLine — e.g. --note-add="./src/h … [line truncated: 33 more bytes on this line] +````` + +## `./build/ripwire . --note-add="lessByScoreDescIdd: typo"` + +*A name that resolves to nothing is refused with the read verbs' own did-you-mean — never written as a dead note.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByScoreDescId'?) — a note keys the canonical id a read verb resolves; to note a FILE instead, pass a path (one with a '/' or an extension), which may name a file that does not exist yet +````` + +## `./build/ripwire . --note-add="src/infra/sortutil.h::rw::sortutil::lessByScoreDescId: chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort"` + +*The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-09-14 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort f8d4313969bf8d93ec05dd1e122d3d7c679b6643 HEAD +````` + +## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` + +*Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h` + +*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.* + +````` +{"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":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], +"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: bash test/greptiercheck.sh +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check` + +*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.* + +````` +{"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":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha (post-check skipped); next: --edit-check=src/infra/sortutil.h:lessByScoreDescId +````` + +## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` + +*Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* + +````` +{"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":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 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":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}], +"order":"evidence","partners":0,"tests":4,"script_gates_unmodelled":675,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: --uses=src/infra/sortutil.h:nonNegativeFloatDescKey +````` + +## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h` + +*An unknown TARGET refuses and leaves every file byte-identical.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --dry-run` + +*A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* + +Input file: + +````` +{ + "version": 1, + "edits": [ + { + "op": "insert_before_symbol", + "target": "nonNegativeFloatDescKey", + "payload": "plan_note.h" + } + ] +} +````` + +````` +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false,"o … [line truncated: 352 more bytes on this line] +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` + +*The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multi … [line truncated: 536 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":7928,"graph_unresolved":4925,"graph_unindexed":219,"counts_floor":true}}]} +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json` + +*Neither --dry-run nor --apply: the mode is explicit, so this refuses.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --edit-plan requires exactly one of --dry-run or --apply +````` + +## `./build/ripwire . --quality-delta` + +*After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 2.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [61 more display lines; full output is 15762 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:88 (was=0 now=59) +````` + +## `./build/ripwire . --quality-baseline` + +*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.* + +**exit code: 1** — **wall time: 1.52s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against HEAD — pinning here would absorb + them into the floor, and every later --quality-delta would read clean. First: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey (was=0 now=59) + Commit the tree first, or pass --allow-dirty to pin anyway (the sidecar then records the 2 absorbed, and every + report against it carries baseline_absorbed="2"). +````` + +## `./build/ripwire . --quality-baseline --allow-dirty` + +*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.* + +**wall time: 2.32s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 18085 per-symbol rows, 18701 indexed symbols total) +````` + +Artifact written: + +````` + 3054018 .ripwire_quality_baseline +# ripwire quality baseline v5 — regenerate with --quality-baseline; do not hand-edit +head f8d4313969bf8d93ec05dd1e122d3d7c679b6643 +dirty 1 +absorbed 2 +ccx d19d7c13cbc7 1 +ccx 1f7edbff929e5 29 +ccx 6a732f6699bdb 0 +ccx 94c5dbe9ae9a6 21 +ccx cac92a0451eb9 0 +ccx 156cfc56beb697 0 +ccx 1599ac5c991b5e 0 +ccx 1 +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 2.37s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 13056 bytes on 1 raw line(s)] +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"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: 742 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: 1111 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, declined_calls, counts_floor}; ca … [line truncated: 874 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). pattern = the literal; in=any lifts the span tiering (by default the tightest non-empty tier is served and what was held back rides as suppressed_comment/suppressed_str … [line truncated: 827 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional ceiling (the CLI --for --token-budget). The header carries route= (which ranker answere … [line truncated: 985 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 657 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 783 more bytes on this line] +{"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, ranked by PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sites calls miss. Call edges … [line truncated: 1306 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"path_between","description":"Does A REACH B, and HOW? — the shortest directed CALL path between two symbols, hop-by-hop. reachable=\"0\" hops=\"0\" is a valid 'not reachable' answer — call edges are name-based, so a missing dynamic/callback edge can hide a real path. Named path_between … [line truncated: 709 more bytes on this line] +{"name":"connect","description":"When a task touches 2..16 named symbols, returns the minimal subgraph RELATING them - terminals, the fewest joining intermediaries (with signatures), and call edges in true direction - finding the shared-caller joins a directed path_between cannot; unrelated symbols … [line truncated: 950 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 1933 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1226 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1204 more bytes on this line] +{"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] +{"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] +{"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] +{"name":"slice","description":"WHERE IS THIS VARIABLE DEFINED AND USED inside one function — NAME-BASED intra-procedural def-use rows of one variable inside ONE uniquely-resolved definition (the ARISE slicer, arXiv:2605.03117). symbol alone lists the sliceable locals to pick from; add var (or spel … [line truncated: 1722 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 1995 more bytes on this line] +````` + +The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: + +````` +tools= 31 manifest_bytes= 43466 (~tokens at 4 bytes/token: 10866 ) +batch desc_bytes= 1190 schema_bytes= 962 required=['queries'] +explore desc_bytes= 1322 schema_bytes= 774 required=['task'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 802 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +from_trace desc_bytes= 829 schema_bytes= 559 required=['trace'] +whereis desc_bytes= 655 schema_bytes= 714 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +find_symbol desc_bytes= 682 schema_bytes= 590 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +for desc_bytes= 445 schema_bytes= 709 required=['task'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +find_referencing_symbols desc_bytes= 434 schema_bytes= 590 required=['symbol'] +grep desc_bytes= 435 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +quality_delta desc_bytes= 749 schema_bytes= 193 required=[] +path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] +cochange desc_bytes= 388 schema_bytes= 476 required=['file'] +analyze desc_bytes= 546 schema_bytes= 318 required=[] +lego desc_bytes= 360 schema_bytes= 467 required=['type'] +mentions desc_bytes= 305 schema_bytes= 486 required=['symbol'] +quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` + +*MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* + +````` +{"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":{"content":[{"type":"text","text":""}], +"_index":"[index: files=2039 symbols=18699 hash=d25eb68a]","_fresh":"ok"}} +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"batch","arguments":{"path":".","queries":["for:incremental cache invalidation","callers:rankGraphTeleport"]}}}' | ./build/ripwire --mcp` + +*The CLI --batch spelling handed to MCP `batch`: refused, with the accepted shape named.* + +````` +{"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":{"content":[{"type":"text","text":""}], +"_index":"[index: files=2039 symbols=18699 hash=d25eb68a]","_fresh":"ok"}} +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"edit_check","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' | ./build/ripwire --mcp` + +*MCP `edit_check` on a CLEAN tree.* + +````` +{"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":{"content":[{"type":"text","text":"