diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 06ac7ac6f..c21b4c866 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -143,6 +143,7 @@ ack api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the ack api-surface eed0ae8252dc773c 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface efd276ea4e37021f 6 cid=8f6887043197d2a3 lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. ack api-surface efe685bb50482275 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack api-surface f3ea5dbe8b5720cb 4 cid=56f3edf8a5c127b0 present-only route= clause: appendCompactForLegend takes withRouteAttr so the compact dialect's route= reading rides only when the root carries the attribute, the same condition the default dialect uses — a file-internal inline helper, and the parameter IS the rule ack api-surface f6dedd851e4b2a25 10 cid=8b170f73ca092237 or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack api-surface f8d747d123b4143d 3 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface f8e53623872b84ac 3 wrap skills-line probe + use-when blurb (audit fix): +1 param on wrapPrintSkillsLine is the deliberate exeDir-probe contract (prebuilt staged-copy fallback, gated by wrapverbscheck §7); churn=self on wrapPrintSkillsLine/wrapEmitAgent is this feature's own edit window on the recently-landed wrap surface diff --git a/CHANGELOG.md b/CHANGELOG.md index 2588dbfa5..d78299c44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,190 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### 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, @@ -337,6 +521,131 @@ 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. + ### Added — Elixir module and arity resolution (parser version 95) Elixir calls now resolve by module, name and arity, with lexical aliases, filtered imports, default diff --git a/README.md b/README.md index 503a4c341..543e10b82 100644 --- a/README.md +++ b/README.md @@ -735,15 +735,14 @@ call a CLI. What comes back — real output from this repository, pretty-printed and trimmed (re-captured 2026-09-05; rows are served in rank order, each naming its file) ```xml - 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. …inline bool ingestCommitTree( const std::string& root, const std::string& sha, … ) - P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) + 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. …inline bool ingestCommitTree( const std::string& root, const std::string& sha, … ) + P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) @@ -1244,12 +1243,12 @@ $ ripwire . --top-k=3 precise=… skipped_oversize=… order=important-first --> - - - + + + - + ``` @@ -1805,9 +1804,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-614 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +615 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **614 gate scripts** and is the authoritative list; +`test/regression.sh` names **615 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2046,8 +2045,9 @@ Publication date: 2026-09-12. Every claim below re-verified against a 0.6.0 buil 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. It has no runtime dependencies. No API key, no embeddings, no index -server, no daemon. It uses a network only when you give it a git URL instead of a directory, which +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. This guide tells you how to install, operate, and evaluate ripwire. Read `docs/COMMANDS.md` for the diff --git a/bench/shotgun/cc_static.py b/bench/shotgun/cc_static.py index e70aaf073..07687b9b8 100644 --- a/bench/shotgun/cc_static.py +++ b/bench/shotgun/cc_static.py @@ -14,7 +14,12 @@ for f in root.iter('f'): fp = f.get('p') for s in f.findall('s'): - key = s.get('id') or f"{fp}::{s.get('n')}" + # ROW 6 (PR #215 review item 9): the map row carries sc= (the enclosing scope), not the path-repeating + # id=. s.get('id') is None on every row now, so this fell through to f"{fp}::{name}" and COLLAPSED two + # same-named methods of different classes in one file into one key — a silent miscount in a benchmark. + # The canonical id is composed the way the legend says: p::sc::n. + sc = s.get('sc') + key = s.get('id') or (f"{fp}::{sc}::{s.get('n')}" if sc else f"{fp}::{s.get('n')}") if s.get('t') in ('fn', 'method', 'macro'): defs[s.get('n')].append((fp, key, s.get('t'))) syms.append((fp, s.get('n'), key, s.get('t'), [c.get('n') for c in s.findall('c')])) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index ceea34ab8..a74a8f354 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-12.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-09-13.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. @@ -68,19 +68,19 @@ _Same map, capped to the 5 highest-ranked symbols._ ``` $ ./build/ripwire . --top-k=5 - + - - + + - + - + ... [10 more line(s); run it to see the whole thing] ``` @@ -104,7 +104,7 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ ``` $ ./build/ripwire . --max-tokens=1500 - + @@ -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` @@ -183,19 +183,21 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - + + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] -#### The convergence disclosure contract - … [line truncated: 85 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` -... [19 more line(s); run it to see the whole thing] + +& p, float alpha = 0.85f ) +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--top-k`, `--max-tokens`, `--token-budget`, `--signatures-only`, `--auto-bodies`, `--no-route`, `--adaptive`, `--no-mention-boost` @@ -218,14 +220,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 - … [line truncated: 85 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -250,19 +252,20 @@ _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 ) 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 -... [19 more line(s); run it to see the whole thing] +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 48> kLangTable = +... [18 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for` @@ -276,7 +279,7 @@ $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies **Answers:** (with --for) skip the query-shape router and always rank by subtoken+body BM25 (with --for/--query) force plain subtoken+body BM25. -Routing is now the DEFAULT: a deterministic, confidence-gated query-shape router picks name-exact BM25 when the query NAMES a symbol (identifier syntax, or every content word is a symbol name) else subtoken+body, and prints which/why in the header. It only routes with a query (the plain map is unaffected). --no-route restores the old behavior. A name-exact header also names its EVIDENCE: anchors: word(defining/file) per anchoring word, +N when N further definitions share that name, or word(syntax) when the word routed on camel/snake SHAPE and names nothing. Paths deeper than two segments print top/.../basename. Discount a one-use test helper yourself. Routing also carries the QUERY-SHAPE document demotion: when the task text parses as a stack trace, sanitizer report or compiler diagnostic, or as a pasted issue-template form, the DOCUMENT tier scores down (repo meta-prose - issue templates, CONTRIBUTING, changelogs - twice as hard) and route= names the shape, its evidence and both factors. Demotion, never exclusion, and the mention anchor still lifts a document the task NAMES. --no-route has no route= to disclose it in, so it does not demote either. +Routing is now the DEFAULT: a deterministic, confidence-gated query-shape router picks name-exact BM25 when the query NAMES a symbol (identifier syntax, or every content word is a symbol name) else subtoken+body, and prints which/why in the header. It only routes with a query (the plain map is unaffected). --no-route restores the old behavior. A name-exact header also names its EVIDENCE: anchors: word(defining/file) per anchoring word, +N when N further definitions share that name, or word(syntax) when the word routed on camel/snake SHAPE and names nothing. Paths deeper than two segments print top/.../basename. Discount a one-use test helper yourself. Routing also carries the QUERY-SHAPE document demotion: when the task text parses as a stack trace, sanitizer report or compiler diagnostic, or as a pasted issue-template form, the DOCUMENT tier scores down (repo meta-prose - issue templates, CONTRIBUTING, changelogs - twice as hard) and route= names the shape, its evidence and both factors. Demotion, never exclusion, and the mention anchor still lifts a document the task NAMES. --no-route has no route= to disclose it in, so it does not demote either. route= is a CODE: name-exact(X) = the task names symbol X (the anchors: clause after it is the evidence); subtoken+body = the conceptual ranker over names and bodies; subtoken+body:broad = a one- or two-word query where plain rg may also win; subtoken+body:declined(word;N-carriers,M-defs) = a name hit refused because the word is a common name (N names carry it, M definitions); a shape demotion appends "; doc tier demoted (...)". On the rows, sc= is the enclosing scope and the full id is p::sc::n (p= of the row or its ) - the spelling --expand/--callers/--impact/--uses accept. **Try it** @@ -284,24 +287,26 @@ _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… -... [20 more line(s); run it to see the whole thing] +struct RankedGraph +... [19 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** - (with --for) skip the query-shape router and always rank by subtoken+body BM25 (with --for/--query) force plain subtoken+body BM25. - Demotion, never exclusion, and the mention anchor still lifts a document the task NAMES. +- subtoken+body:declined(word;N-carriers,M-defs) = a name hit refused because the word is a common name (N names carry it, M definitions); ### `--adaptive` @@ -315,18 +320,20 @@ _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 ) 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 -... [20 more line(s); run it to see the whole thing] +enum class AstWalk : std::uint8_t +constexpr std::array<LangEntry, 48> kLangTable = +... [18 more line(s); run it to see the whole thing] ``` **Shaped by:** `--for`, `--detail` @@ -347,17 +354,18 @@ _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 - - - - + + + + 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 ) - -... [21 more line(s); run it to see the whole thing] +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 +... [20 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -377,15 +385,15 @@ _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 ) -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] - + + + + +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOfScope, st … [line truncated: 7 more bytes on this line] + runQualityViews was NOT a dispatch chain — it held two branches, one of which was 298 lines. T…std::optional<int> runQualityDelta( const MainDispatch& d ) - + WHAT this delta is measured AGAINST, and on WHICH tree — the one place --quality-delta decides…struct DeltaBasis ... [21 more line(s); run it to see the whole thing] ``` @@ -409,7 +417,7 @@ $ ./build/ripwire . --lego=Vehicle - + @@ -438,7 +446,7 @@ _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 ) @@ -468,9 +476,9 @@ _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=5570 +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.576) ━━ [sections: 1 of 10 selected (10 in doc), section-granular; whole doc 30213 B; lines="121-142"; dropped_by_budget=9] [truncated: 2098 of 9770 bytes] +━━ 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 @@ -533,20 +541,25 @@ 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 1767798-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 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._ ``` $ ./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=2041 est_tokens=951 +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 -━━ commands.md (relevance 9.779) ━━ [sections: 1 of 150 selected (179 in doc), section-granular; whole doc 691660 B; lines="3653-3657"; dropped_by_budget=149] [truncated: 1865 of 7256 bytes] -### `--field-affinity[=STRUCT]` +━━ 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 -**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. +**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? -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] +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: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens or narrow the query for 1 more (~2548-byte budget)) +... [17 more line(s); run it to see the whole thing] ``` ### `--tree` @@ -563,7 +576,7 @@ _File-by-file orientation map (top symbols per file)._ $ ./build/ripwire . --tree - + @@ -633,18 +646,18 @@ _Stable (path/id) emit order — provider KV-cache hits across re-runs._ ``` $ ./build/ripwire . --order=stable --top-k=5 - + - + - + - + ... [10 more line(s); run it to see the whole thing] @@ -664,19 +677,19 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable ``` $ ./build/ripwire . --no-stable --top-k=3 - + - - + + - + - + ... [2 more line(s); run it to see the whole thing] ``` @@ -697,7 +710,7 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - + @@ -705,10 +718,10 @@ $ ./build/ripwire . --around=rankGraphTeleport - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -731,7 +744,7 @@ $ ./build/ripwire . --callers=DoesNotExist (empty) ``` -**Shaped by:** `--callees`, `--uses`, `--impact`, `--expand`, `--edit-check`, `--slice-flow`, `--at`, `--format` +**Shaped by:** `--no-route`, `--callees`, `--uses`, `--impact`, `--expand`, `--edit-check`, `--slice-flow`, `--at` **Caveats (stated by the binary):** @@ -751,7 +764,7 @@ _What SYM calls (1-hop out-edges)._ $ ./build/ripwire . --callees=rankGraphTeleport - + @@ -784,7 +797,7 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count $ ./build/ripwire . --uses=rankGraphTeleport - + @@ -797,7 +810,7 @@ $ ./build/ripwire . --uses=rankGraphTeleport ``` -**Shaped by:** `--impact`, `--naming-consistency`, `--edit-check`, `--edit-target-file`, `--safe-delete`, `--doctor`, `--format`, `--legend` +**Shaped by:** `--no-route`, `--impact`, `--naming-consistency`, `--edit-check`, `--edit-target-file`, `--safe-delete`, `--doctor`, `--format` **Caveats (stated by the binary):** @@ -818,16 +831,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - - + + - + @@ -855,19 +868,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] ``` @@ -891,10 +904,10 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d $ ./build/ripwire . --path=main,rankGraphTeleport - - - - + + + + @@ -915,7 +928,7 @@ $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + @@ -941,23 +954,23 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - - + + - + ... [17 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--uses`, `--metrics`, `--deps`, `--safe-delete`, `--slice-flow`, `--format`, `--legend`, `--json` +**Shaped by:** `--no-route`, `--uses`, `--metrics`, `--deps`, `--safe-delete`, `--slice-flow`, `--format`, `--legend` **Caveats (stated by the binary):** @@ -1026,7 +1039,7 @@ _Test files that transitively reach the changed file._ $ ./build/ripwire . --affected=src/graph.h - + @@ -1086,7 +1099,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: 7a4063384 +at: 3840b6cb8 (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1105,18 +1118,18 @@ _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] ``` @@ -1140,7 +1153,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1163,7 +1176,7 @@ _Regex search + enclosing symbol._ ``` $ ./build/ripwire . --regex='fnv1a\w+' - @@ -1192,7 +1205,7 @@ _Same search with one line of source context either side._ ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-context=1 - @@ -1221,7 +1234,7 @@ _Boolean grep: hits where BOTH literals share the matched line (--grep-scope=lin ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache - @@ -1250,7 +1263,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 - @@ -1281,7 +1294,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 - @@ -1314,7 +1327,7 @@ _Span tiers off: the exhaustive view — the comment and string hits the default ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-in=any - @@ -1351,7 +1364,7 @@ _h= on each editable enclosing-symbol row: a freshness-pinned identity an edit v $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --handles - @@ -1384,7 +1397,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 @@ -1432,17 +1445,17 @@ _Raw BM25 ranking (debug lens; --for is the real verb)._ ``` $ ./build/ripwire . --query="teleport pagerank" --top-k=5 - - + + - - + + - + @@ -1467,18 +1480,19 @@ _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 - -... [20 more line(s); run it to see the whole thing] + +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] ``` **Shaped by:** `--max-tokens`, `--for`, `--signatures-only`, `--auto-bodies`, `--compress`, `--owners`, `--plan`, `--abi` @@ -1495,24 +1509,24 @@ test/showcasecapturecheck.sh (C) re-derives this range from the SAME repo every **Try it** -_Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 89.5% fewer bytes at top-10, 81.8% at top-50, 84.3% at top-100 (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._ +_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._ ``` $ ./build/ripwire . --pack-signatures --top-k=10 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1553,7 +1567,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 **Answers:** print these symbols' full bodies, plus the signatures of what they call full bodies of A,B,... -Selector grammar per item (the tail after the LAST ':' decides; a tail STARTING WITH A DIGIT is a range, anything else is a name): NAME every def of that name | FILE:NAME that file's def NAME:START-END body slice | FILE:NAME:START-END selector + slice FILE:LINE:NAME paste a row's p="path:line" straight from --callers/--lint/--grep (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its own p=/top_l=/top= instead, since top= is just the worst function's name) path::scope::name the canonical id= --for/--pack-task emit Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid); a wrong scope refuses, it never falls back to the bare-name union (START-END is 1-based within the def's OWN body — lines="lo-hi/total" marks the slice partial; out-of-range clamps. FILE matches any path substring, like --callers/--lego.) EXACT-NAME DEFAULT (one token, one unambiguous match, no explicit --top-k): the ranked map defaults to top-k=0 — you already named the exact symbol, so the ~200-row orientation map is pure overhead in front of the one body it exists to summarize. Disclosed on the root as topk_default="0" (self-describing: the change is visible without reading source). A MULTI-match name (an ambiguous bare name) or a multi-token --expand keeps the map — there IS something to disambiguate. An EXPLICIT --top-k=N (0 included) always overrides this default. Each body also carries sibs="a,b,..." sibs_total="N" [sibs_capped="1"] (the file's OTHER symbols, names only, capped at 40) and inc="x.h,..." inc_total="N" [inc_capped="1"] (the file's own #include/import targets, capped at 24) — both absent when the count is 0 (a documented zero, not a degrade), so a body no longer needs a second --outline call just to learn what else lives in its file. CHEAPEST-COMPLETE-ANSWER SERVING (no explicit --top-k, no range slice): the verb ALSO measures the (possibly map-less) bundle against the requested symbols' whole FILE(s) and emits the SMALLER, disclosed on the root as mode="bundle|whole-file" reason="the two byte counts" — on a small file the old bundle was 5.65x the file itself; on a big file the bundle saves ~26x. The whole-file form is with the file CDATA-wrapped (redacted as usual) and every requested symbol's line anchor kept. An EXPLICIT --top-k=N (including 0) opts out of BOTH the exact-name default and mode= auto-selection and keeps the classic undecorated shape; a SYM:START-END slice opts out of mode= auto-selection only (serving the whole file would invert an explicit narrowing) but still gets the exact-name top-k=0 default when it applies. +Selector grammar per item (the tail after the LAST ':' decides; a tail STARTING WITH A DIGIT is a range, anything else is a name): NAME every def of that name | FILE:NAME that file's def NAME:START-END body slice | FILE:NAME:START-END selector + slice FILE:LINE:NAME paste a row's p="path:line" straight from --callers/--lint/--grep (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its own p=/top_l=/top= instead, since top= is just the worst function's name) path::scope::name the id composed from a row's p= sc= n= (the map, --for, --pack-task) Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid); a wrong scope refuses, it never falls back to the bare-name union (START-END is 1-based within the def's OWN body — lines="lo-hi/total" marks the slice partial; out-of-range clamps. FILE matches any path substring, like --callers/--lego.) EXACT-NAME DEFAULT (one token, one unambiguous match, no explicit --top-k): the ranked map defaults to top-k=0 — you already named the exact symbol, so the ~200-row orientation map is pure overhead in front of the one body it exists to summarize. Disclosed on the root as topk_default="0" (self-describing: the change is visible without reading source). A MULTI-match name (an ambiguous bare name) or a multi-token --expand keeps the map — there IS something to disambiguate. An EXPLICIT --top-k=N (0 included) always overrides this default. Each body also carries sibs="a,b,..." sibs_total="N" [sibs_capped="1"] (the file's OTHER symbols, names only, capped at 40) and inc="x.h,..." inc_total="N" [inc_capped="1"] (the file's own #include/import targets, capped at 24) — both absent when the count is 0 (a documented zero, not a degrade), so a body no longer needs a second --outline call just to learn what else lives in its file. CHEAPEST-COMPLETE-ANSWER SERVING (no explicit --top-k, no range slice): the verb ALSO measures the (possibly map-less) bundle against the requested symbols' whole FILE(s) and emits the SMALLER, disclosed on the root as mode="bundle|whole-file" reason="the two byte counts" — on a small file the old bundle was 5.65x the file itself; on a big file the bundle saves ~26x. The whole-file form is with the file CDATA-wrapped (redacted as usual) and every requested symbol's line anchor kept. An EXPLICIT --top-k=N (including 0) opts out of BOTH the exact-name default and mode= auto-selection and keeps the classic undecorated shape; a SYM:START-END slice opts out of mode= auto-selection only (serving the whole file would invert an explicit narrowing) but still gets the exact-name top-k=0 default when it applies. **Try it** @@ -1561,11 +1575,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + + @@ -1703,12 +1717,12 @@ $ ./build/ripwire . --metrics --top-k=10 - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1733,13 +1747,13 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - + + + - - - + + + @@ -1771,16 +1785,16 @@ $ ./build/ripwire . --hotspots - - - - - - - + + + + + + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1798,18 +1812,18 @@ _Token-normalized duplicate bodies._ $ ./build/ripwire . --clones - - + + + + + + + - - - - - ... [17 more line(s); run it to see the whole thing] ``` @@ -1829,15 +1843,15 @@ _Per-function readability, LEAST readable first (Halstead volume, token entropy, $ ./build/ripwire . --readability --limit=8 - - - - - - + + + + + + + - ``` @@ -1863,7 +1877,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1899,17 +1913,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1930,17 +1944,17 @@ $ ./build/ripwire . --quality-panel - - - - - - - - + + - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1966,13 +1980,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] ``` @@ -1997,7 +2011,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2006,11 +2020,11 @@ $ ./build/ripwire . --naming-calibration - +

-... [12 more line(s); run it to see the whole thing] +... [13 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -2033,11 +2047,11 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio $ ./build/ripwire . --naming-consistency --limit=8 - - - - - + + + + + @@ -2069,8 +2083,8 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - + + @@ -2078,7 +2092,7 @@ $ ./build/ripwire . --lint --naming-locals - + @@ -2104,9 +2118,9 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2139,16 +2153,16 @@ $ ./build/ripwire . --cochange - - + + - + + + - - ... [22 more line(s); run it to see the whole thing] ``` @@ -2173,16 +2187,16 @@ $ ./build/ripwire . --cochange --cochange-recur=2 - - + + - + - - - + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2201,11 +2215,11 @@ $ ./build/ripwire . --cochange --cochange-groups - + - + @@ -2231,16 +2245,16 @@ $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2349,7 +2363,7 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - + @@ -2358,7 +2372,7 @@ $ ./build/ripwire . --lint - + @@ -2459,7 +2473,7 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2468,7 +2482,7 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2546,17 +2560,17 @@ _Cluster the call graph into cohesive modules._ $ ./build/ripwire . --communities - + + - - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2577,7 +2591,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp $ ./build/ripwire . --community=0 - ``` @@ -2602,17 +2616,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] @@ -2639,16 +2653,16 @@ $ ./build/ripwire . --report # ripwire architecture report -2029 files · 18359 symbols · 21522 edges · 1282 modules (11418 call-graph isolated) +2033 files · 18474 symbols · 21771 edges · 1285 modules (11480 call-graph isolated) Root: `.` -Call-graph isolate provenance: 2586 declaration, 1143 header, 3612 source, 4077 document; 0 connected Louvain singletons +Call-graph isolate provenance: 2608 declaration, 1150 header, 3637 source, 4085 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 1282) -- **src::emitTo@infra/emit.h:46:2380 [write,run,emit]** — 698 symbols -- **src::VERIFY@infra/Diagnostics.h:200:10893 [compute,resolve,add]** — 651 symbols -- **src::append@elixir_resolve.h:111:4875 [parse,pack,read]** — 468 symbols +## 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 ... [17 more line(s); run it to see the whole thing] ``` @@ -2666,12 +2680,12 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ $ ./build/ripwire . --seams - - + + + - @@ -2696,11 +2710,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
5283"] - n99["src/infra
620"] + n98["src
5351"] + n99["src/infra
621"] end subgraph sg1 ["test"] - n100["test
3434"] + n100["test
3467"] n269["test/fixtures/recallpassage
314"] n255["test/expandmodefix
151"] n257["test/expandsibsfix
149"] @@ -2727,14 +2741,14 @@ $ ./build/ripwire . --owners - - - + + + - + - + @@ -2756,7 +2770,8 @@ _High-confidence internal functions with no caller. NOTE the filter is a path-CO ``` $ ./build/ripwire . --dead-code=src - + + ``` @@ -2820,9 +2835,10 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - + @@ -2832,7 +2848,6 @@ $ ./build/ripwire . --quality-delta - ... [17 more line(s); run it to see the whole thing] ``` @@ -2857,9 +2872,10 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - + @@ -2869,7 +2885,6 @@ $ ./build/ripwire . --quality-delta - ... [17 more line(s); run it to see the whole thing] ``` @@ -2894,7 +2909,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -2964,7 +2979,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -2994,7 +3009,7 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - + @@ -3047,11 +3062,11 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"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":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":667,"graph_ambiguous":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"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"} ``` ### `--insert-after-symbol=TARGET` @@ -3067,11 +3082,11 @@ _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":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":667,"graph_ambiguous":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true,"next":"bash test/greptiercheck.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"} ``` **Caveats (stated by the binary):** @@ -3117,11 +3132,11 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"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":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"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"}, {"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":667,"graph_ambiguous":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"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"} ``` **Shaped by:** `--insert-after-symbol` @@ -3180,11 +3195,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":7709,"graph_unresolved":4847,"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: 594 more bytes on this line] +{"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] {"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":7709,"graph_unresolved":4847,"graph_unindexed":218,"counts_floor":true}}]} +"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3229,7 +3244,7 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - + @@ -3267,7 +3282,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() );]]> @@ -3347,7 +3362,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - + ``` @@ -3373,11 +3388,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3447,20 +3462,12 @@ Per local ref (SUBSTR filters ref names): the lines its own divergent work AUTHO **Try it** -_A second selection, the one branch `main` (no ref family left to select on this checkout), 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=._ +_Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts._ ``` -$ ./build/ripwire . --stray-content=main +$ ./build/ripwire . --stray-content=lane/ - - - - - - - - - + ``` @@ -3485,20 +3492,8 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - - - - - - - - - - - - - -... [17 more line(s); run it to see the whole thing] + + ``` **Shaped by:** `--stray-content`, `--legend`, `--json` @@ -3521,7 +3516,7 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - + ``` @@ -3544,12 +3539,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] ``` @@ -3575,7 +3570,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3656,7 +3651,7 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - @@ -3691,17 +3686,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] ``` @@ -3726,17 +3721,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] ``` @@ -3760,18 +3755,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] ``` @@ -3796,7 +3791,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3834,7 +3829,7 @@ AddressSanitizer:DEADLYSIGNAL #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:2880 + #3 0x102e01a44 in main src/main.cpp:2988 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) ==41337==ABORTING ``` @@ -3861,7 +3856,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -3951,18 +3946,21 @@ _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] - + 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 -... [20 more line(s); run it to see the whole thing] +struct AgentTarget + +verifier N2/N3/N11: the bad-VALUE refusal tablestruct McpValueSpec + +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--top-k`, `--token-budget`, `--for`, `--test-gate`, `--expand`, `--compress`, `--no-redact`, `--partition` @@ -3985,19 +3983,20 @@ _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 - is one agent's ctx. requested=N asked, partitions= carved; modules=/split= groups found / cuts forced; core_symbols= the shared core, surface= core plus the assignable remainder; budget_per_agent_tokens= core plus ONE partition (= core_budget_tokens + partition_budget_tokens), total_bytes= all bundles; overlap_mean=/overlap_max= pairwise Jaccard over the ids partitions name, pre-trim; shared_symbols= the ids TWO OR MORE partitions name, union_symbols= ids any names (ratio and overlap_mean COINCIDE at partitions=2 only); core_overlap= the core surface a partition reaches anyway; tokens= = est_tokens= (bytes / 2.36). Each ctx carries NO legend: the task-bundle legend follows once; a trimmed slice carries one data comment. --> - - - + + + - + 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… - -... [19 more line(s); run it to see the whole thing] + +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] ``` **Caveats (stated by the binary):** @@ -4018,18 +4017,19 @@ _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 - -... [20 more line(s); run it to see the whole thing] + +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] ``` **Shaped by:** `--partition` @@ -4093,14 +4093,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + ``` @@ -4126,14 +4126,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4161,7 +4161,7 @@ $ ./build/ripwire . --skipped - @@ -4238,7 +4238,7 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + @@ -4247,10 +4247,10 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 - - + + - + ... [16 more line(s); run it to see the whole thing] ``` @@ -4313,7 +4313,7 @@ _The same gating report under --legend=compact — same rows, same exit 2, schem ``` $ ./build/ripwire . --quality-delta --legend=compact - @@ -4363,17 +4363,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4395,19 +4395,19 @@ _Drop matching paths (repeatable) before ranking._ ``` $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - + - - + + - + - + ... [10 more line(s); run it to see the whole thing] ``` @@ -4426,7 +4426,7 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - + @@ -4434,12 +4434,12 @@ $ ./build/ripwire . --map-diff --top-k=5 - - + + - + - + ... [11 more line(s); run it to see the whole thing] ``` @@ -4460,19 +4460,19 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ ``` $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - + - - + + - + - + ... [2 more line(s); run it to see the whole thing] ``` @@ -4510,19 +4510,19 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co ``` $ ./build/ripwire . --no-cache --top-k=3 - + - - + + - + - + ... [2 more line(s); run it to see the whole thing] ``` @@ -4541,19 +4541,19 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses ``` $ ./build/ripwire . --no-ignore --top-k=3 - + - - + + - + - + ... [2 more line(s); run it to see the whole thing] ``` @@ -4575,17 +4575,17 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea ``` $ ./build/ripwire . --max-file-size=8K --top-k=3 - + - - + + - + @@ -4641,19 +4641,19 @@ _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and th ``` $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - + - - + + - + - + ... [2 more line(s); run it to see the whole thing] ``` @@ -4669,7 +4669,7 @@ _initialize + tools/list: the manifest an agent host loads at session start — ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a symbol; run edit_c … [line truncated: 368 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 715 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] ... [28 more line(s); run it to see the whole thing] @@ -4720,9 +4720,9 @@ _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/budget-scale-floor-2026-09-10 merged -lane/calls-rank-order-2026-09-10 unmerged -lane/cap-disclosure-2026-09-10 merged +lane/sc-legend merged +no-such-ref-1 unmerged +no-such-ref-2 merged ``` ### `--eval` @@ -4737,14 +4737,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 1.9% 3.6% 10.6% - BM25 15.1% 17.7% 18.6% - BM25sub 13.8% 17.8% 20.5% - BM25body 22.7% 33.5% 47.7% - fused 5.3% 19.5% 24.7% - anchored 21.5% 33.5% 47.7% - same-dir 2.1% 4.5% 5.2% - random 0.2% 0.5% 1.0% <- floor (random ranking over F=2029 files) + 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) 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 / @@ -4767,19 +4767,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=4081 scored=4000 rule=smallest-key CAPPED — a SUBSET, not the population + sample: population=4115 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.715 58.2% 88.1% 92.7% - subtoken doc-phrase 0.930 90.9% 95.1% 95.7% - name-exact name 0.920 85.5% 97.1% 98.2% - name-exact doc-phrase 0.014 0.5% 2.2% 2.8% - anchored name 0.719 59.5% 87.2% 91.5% - anchored doc-phrase 0.925 90.1% 94.9% 95.7% - routed name 0.921 85.5% 97.1% 98.2% - routed doc-phrase 0.928 90.8% 94.9% 95.4% + 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% ... [6 more line(s); run it to see the whole thing] ``` @@ -4825,7 +4825,7 @@ _Version + short build info._ ``` $ ./build/ripwire --version -ripwire 0.6.0 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=0438f8cfc) +ripwire 0.6.0 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=88355acd0+dirty) ``` **Shaped by:** `--impact`, `--verify`, `--metrics`, `--deps`, `--naming-locals`, `--arch`, `--dry-run`, `--doc-drift` diff --git a/docs/EVALS.md b/docs/EVALS.md index 073cf7742..360d7698b 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 614 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 615 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5834,7 +5834,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **614 gate scripts**, all of which exist on disk. +naming **615 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6846,7 +6846,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 614. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 615. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LINEAGE.md b/docs/LINEAGE.md index 7515ecab3..9671457c5 100644 --- a/docs/LINEAGE.md +++ b/docs/LINEAGE.md @@ -353,7 +353,7 @@ Nothing in the tables above. The parts that are not borrowed are the constraints - **Determinism as a contract, not a tendency.** Two runs byte-identical, and a warm run byte-identical to a cold one, gated on every push. - **Every count that cannot be proven total says so, in the output.** `counts_floor=`, `shown_*`, `*_capped=`, `amb=` — the vocabulary is defined by a legend the document carries with it. - **An advertised number is an enumerated, gated number.** Including the three in this document's own header. -- **One compiled binary, no runtime dependencies, no embeddings, no server, no network.** Each of those is a thing several tools above have and this one does not, which is a trade and is described as one. +- **One compiled binary, no runtime dependencies for the map, no embeddings, no server, and no network for a local root — the one exception is a `` root, which shallow-clones before it maps.** Each of those is a thing several tools above have and this one does not, which is a trade and is described as one. - **The process that keeps the other four true.** Every document that makes claims — the tool's own output included — is treated as a claims corpus and adversarially audited; a gate must be shown able to fail before its green is trusted; and eighteen consecutive audit rounds have each found real defects, including in this document. The method ships as [`METHODOLOGY.md`](METHODOLOGY.md), and a paper writing it up as transferable method is in preparation — the working draft lives in [`paper/`](../paper/). The combination is the contribution. The pieces are everybody's. diff --git a/docs/captures/COMMANDS_showcase_2026-09-12.md b/docs/captures/COMMANDS_showcase_2026-09-12.md index ee9440550..9898fbb60 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-12.md +++ b/docs/captures/COMMANDS_showcase_2026-09-12.md @@ -1376,7 +1376,7 @@ at: 7a4063384 ## `./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: 89.5% fewer bytes at top-10, 81.8% at top-50, 84.3% at top-100 (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.* +*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.* ````` diff --git a/docs/captures/COMMANDS_showcase_2026-09-13.md b/docs/captures/COMMANDS_showcase_2026-09-13.md new file mode 100644 index 000000000..2a9b161c9 --- /dev/null +++ b/docs/captures/COMMANDS_showcase_2026-09-13.md @@ -0,0 +1,7152 @@ +# ripwire — every verb, run for real + +- **Date:** 2026-09-13 (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.0 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=88355acd0+dirty)` +- **Repo:** the ripwire repo @ `3840b6cb` — **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` (197 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.* + +**wall time: 1.73s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [807 more display lines; full output is 22407 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=9049 > 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.* + +**wall time: 3.25s** + +````` + + + + + +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] +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 = n… +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 ) +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` +struct ChurnRanking + +… [136 more display lines; full output is 15877 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 +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +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 +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [53 more display lines; full output is 9894 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.* + +**wall time: 1.11s** + +````` + + + + +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 10136 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.* + +**wall time: 1.41s** + +````` + + + + +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 10222 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=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*". +2. **Make your change.** +3. **Measure the delta** — `ripwire --quality-delta --legend=compact` → only the regressions you + introduced, across the + 10 kinds in the table below. Each emits `` (`members=` for duplication). + Test-fixture dirs are exempt from `dead-code`; `short-horizon-churn` ignores your own current edit and + exempts brand-new symbols/markdown/fixtures. Two exemptions are DISCLOSED on the report rather than + silent, and both change how you read a zero: a symbol defined by a self-registering test/benchmark + macro (doctest `TEST_CASE`, gtest `TEST`/`TEST_F`/`TEST_P`, Catch2, Google Benchmark, plus anything in + `.ripwire_config`'s `register_macros`) is never `dead-code` — a static initializer is invisible to a + name-based call graph — and the header's `register-macro-excluded="N"` counts how many were dropped + that way, printed even at 0. … + +━━ README.md (relevance 6.310) ━━ [sections: 2 of 18 selected (60 in doc), section-granular; whole doc 168232 B; lines="2345-2353,2448-2468"; dropped_by_budget=16] +#### 6.2 Exit codes + +| Code | Meaning | +… [177 more lines, 14271 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 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.* + +````` +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 + +━━ 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 + +**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? + +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: + +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. + +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … + +(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 11756 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 1.15s** + +````` +(empty) +````` + +Artifact written: + +````` + 158567 /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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [211 more display lines; full output is 9600 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [3127 more display lines; full output is 83457 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 5357 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [24 more display lines; full output is 5247 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 5545 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 5507 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 7889 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: 3840b6cb8 + (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.* + +**wall time: 2.02s** + +````` + + +````` + +## `./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; }]]> + + + + +… [568 more display lines; full output is 35477 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1172 more display lines; full output is 45601 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [29 more display lines; full output is 4130 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.* + +**wall time: 1.11s** + +````` + + + + + +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 ) +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] +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… +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +## Background — read these, in this order +## Design space and constraints +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) +handle_t leftmost_leaf() const +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] +## Why it matters +… [123 more display lines; full output is 14705 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [142 more display lines; full output is 13056 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [837 more display lines; full output is 21987 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 1.68s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [16 more display lines; full output is 6614 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 3.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [310 more display lines; full output is 18708 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 6610 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, ...).* + +**wall time: 1.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [692 more display lines; full output is 71118 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 17154 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 8409 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [249 more display lines; full output is 12328 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +2033 files · 18474 symbols · 21771 edges · 1285 modules (11480 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 2608 declaration, 1150 header, 3637 source, 4085 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 +- **src::DEGRADED_PATH_ALERT@infra/Diagnostics.h:186:10129 [compute,read,open]** — 173 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [emit,collect,append]** — 156 symbols +- **src::PROFILE_SCOPE_DESCRIBE@infra/profileScope.h:1326:45284 [emit,check,resolve]** — 57 symbols +- **src::relForHash@arch.h:557:26393 [compute,clone,read]** — 53 symbols +- **src::str@ingest_cache.h:1484:140148 [read,write,scan]** — 53 symbols +- **src/infra::active@profilePmc.h:845:34910 [print,ensure,report]** — 40 symbols +- **src/infra::buf@svector.h:125:8942 [insert,pack,move]** — 38 symbols +- **src::sliceKindIs@slice.h:295:19740 [match]** — 33 symbols +- **src/infra::radixSortByScoreDescId@sortutil.h:97:3893 [append]** — 31 symbols + +## God files (most depended-on; showing 10 of 353) +- `src/infra/emit.h` — 84 dependents +- `src/model.h` — 80 dependents +- `src/infra/Diagnostics.h` — 47 dependents +- `src/serialize.h` — 35 dependents +- `src/graph.h` — 33 dependents +… [32 more lines, 3640 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 12676 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
5351"] + n99["src/infra
621"] + end + subgraph sg1 ["test"] + n100["test
3467"] + 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
775"] + 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.* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [753 more display lines; full output is 88911 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: 34.58s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [51 more display lines; full output is 12379 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").* + +**wall time: 3.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [171 more display lines; full output is 19892 bytes on 1 raw line(s)] +````` + +## `./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: 18.42s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family 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=lane/ --plan` + +*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).* + +````` + + + +````` + +## `./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=lane/ --abi` + +*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.* + +````` + + + +````` + +## `./build/ripwire . --whereis=rankGraphTeleport` + +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch.* + +**wall time: 2.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [34 more display lines; full output is 18441 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: 27.80s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [336 more display lines; full output is 21367 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.* + +**wall time: 1.72s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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** — **wall time: 1.45s** + +````` +(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.* + +**wall time: 3.32s** + +````` +{"v":1,"verb":"plan-lanes","at":"3840b6cb8","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":2033,"symbols":18474,"edges":217 … [line truncated: 357 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":554,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1679,"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/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":1488,"ord":0,"overloads":1,"amb":15,"cx":34,"ccx":69,"churn":33,"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":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":738,"ord":0,"overloads":1,"amb":0,"cx":5,"ccx":4,"churn":17,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":2120,"ord":0,"overloads":1,"amb":33,"cx":8,"ccx":11,"churn":302,"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":30,"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":15,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":617,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":302,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":4576,"ord":0,"overloads":1,"amb":1,"cx":3,"ccx":2,"churn":181,"tested":0}], +"files":[{"p":"./src/crossref.h","symbols":1,"churn":27,"ccx":23,"hotspot_rank":22}, +{"p":"./src/darkflags.h","symbols":1,"churn":15,"ccx":7,"hotspot_rank":49}, +{"p":"./src/docdrift.h","symbols":2,"churn":20,"ccx":11,"hotspot_rank":23}, +{"p":"./src/gitmine.h","symbols":1,"churn":30,"ccx":4,"hotspot_rank":16}, +{"p":"./src/main.cpp","symbols":2,"churn":302,"ccx":12,"hotspot_rank":2}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":181,"ccx":2,"hotspot_rank":5}, +{"p":"./src/pageview.h","symbols":1,"churn":15,"ccx":2,"hotspot_rank":104}, +{"p":"./src/testmap.h","symbols":1,"churn":17,"ccx":4,"hotspot_rank":53}, +{"p":"./src/verbs_change.h","symbols":2,"churn":33,"ccx":77,"hotspot_rank":18}]},"blast_radius":{"reaches":47,"files_total":17,"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: 205 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":9,"module_span":1,"max_ccx":69,"sum_ccx":142,"ambiguous_calls":72,"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] +… [73 more display lines; full output is 20546 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).* + +**wall time: 2.85s** + +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":"3840b6cb8","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":2033,"symbols":18474,"edges":21771,"ambiguous":7815,"unresolved":4873},"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":554,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":181,"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":2951,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":286,"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":1679,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":33,"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":286,"ccx":0,"hotspot_rank":6}, +{"p":"./src/darkflags.h","symbols":1,"churn":15,"ccx":0,"hotspot_rank":49}, +{"p":"./src/docdrift.h","symbols":5,"churn":20,"ccx":49,"hotspot_rank":23}, +{"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":36}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":181,"ccx":0,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":24,"ccx":4,"hotspot_rank":32}, +{"p":"./src/verbs_change.h","symbols":1,"churn":33,"ccx":4,"hotspot_rank":18}]},"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":99,"l":255,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":4,"t … [line truncated: 10 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":2576,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":286,"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":2951,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":286,"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":2655,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":286,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":99,"l":826,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":14,"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":2720,"ord":0,"overloads":1,"amb":2,"cx":4,"ccx":3,"churn":286,"tested":0}, +… [61 more display lines; full output is 18759 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [174 more display lines; full output is 24477 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [197 more display lines; full output is 26270 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.* + +**wall time: 1.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [161 more display lines; full output is 25137 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:1158 + #3 0x102e01a44 in main src/main.cpp:2988 + #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] + +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 + +offsetof, which would be UB on a non-standard-layout type. ORDER. The tables are scanned in DECLARATION ORDER, exacts before prefixes, ahead of the hand-written arms — so the chain's original precedstruct BoolFlag + + + + + + + + + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + +… [47 more display lines; full output is 9303 bytes on 42 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.* + +````` + is one agent's ctx. requested=N asked, partitions= carved; modules=/split= groups found / cuts forced; core_symbols= the shared core, surface= core plus the assignable remainder; budget_per_agent_tokens= core plus ONE partition (= core_budget_tokens + partition_budget_tokens), total_bytes= all bundles; overlap_mean=/overlap_max= pairwise Jaccard over the ids partitions name, pre-trim; shared_symbols= the ids TWO OR MORE partitions name, union_symbols= ids any names (ratio and overlap_mean COINCIDE at partitions=2 only); core_overlap= the core surface a partition reaches anyway; tokens= = est_tokens= (bytes / 2.36). Each ctx carries NO legend: the task-bundle legend follows once; a trimmed slice carries one data comment. --> + + + + + + +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 +struct AgentTarget + + + + + +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 ) +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] +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… +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +## Background — read these, in this order +## Design space and constraints +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) +handle_t leftmost_leaf() const +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] +## Why it matters +… [58 more display lines; full output is 10294 bytes on 11 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +**wall time: 2.01s** + +````` +(empty) +````` + +Artifact written: + +````` + 303761 /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 .* + +**wall time: 3.09s** + +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 26547 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** — **wall time: 3.68s** + +````` + + + + + + + + + +````` + + +--- + +# 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.* + +**wall time: 2.09s** + +````` + +````` + +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.* + +**wall time: 1.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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,3 +runEval,rankGraph,anchoredLexicalRank,churnRankedGraph,runDefaultMap,getIndex +171,3445,3995,1031,1156,1108 +fn,fn,fn,fn,fn,fn +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":6,"root":".","hop_tested":0,"hop_untested":6,"graph_ambiguous":7815,"graph_unresolved":4873,"graph_unindexed":218,"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":"churnRankedGraph","p":"src/main.cpp:1031"}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1156"}, +{"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).* + +**wall time: 1.88s** + +````` + + + + + + + + + +````` + +## `./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.* + +**wall time: 1.54s** + +````` + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3` + +*Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* + +**wall time: 1.82s** + +````` + + + + + + + + + + + + + + + + +````` + +Artifact written: + +````` + 12161861 /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 /.* + +**wall time: 1.40s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 25.19s** + +````` +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) + 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: 117.79s** + +````` +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 + (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% + 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) +lane/sc-legend merged +no-such-ref-1 unmerged +no-such-ref-2 merged +````` + +````` +(empty) +````` + +stderr: + +````` +ripwire: --eval-stray: 2 labelled ref(s) do not exist in . -- not merged, just absent: 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.592) + 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.0 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=88355acd0+dirty) +````` + + +--- + +# 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 6724 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 18215 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [549 more display lines; full output is 32142 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 44799 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; }]]> + + + +… [569 more display lines; full output is 38792 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 +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +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 +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [56 more display lines; full output is 9290 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 +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +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 +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroReparse& w … [line truncated: 9 more bytes on this line] +enum class SpanTier : std::uint8_t +inline bool spanTierMemoTryLoad( bool useMemo, const std::string& diskPath, const StatInfo& now, SpanTierMap& out ) +struct AstQuerySpec +struct McpIndex +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +… [94 more display lines; full output is 13212 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" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +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 9995 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" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +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 10095 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [353 more display lines; full output is 41444 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:1158 + #3 0x102e01a44 in main src/main.cpp:2988 + #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.15s** + +````` + + + + +````` + +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: 2.83s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [187 more display lines; full output is 17919 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: 2.59s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [42 more display lines; full output is 10012 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.* + +**wall time: 1.16s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 11034 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [492 more display lines; full output is 43866 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: 5.25s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [59 more display lines; full output is 9046 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.* + +**wall time: 1.51s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [635 more display lines; full output is 73793 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.* + +**wall time: 1.44s** + +````` + + + + + + + + + + +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] ] +… [411 more display lines; full output is 52213 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.* + +**wall time: 1.31s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [725 more display lines; full output is 67021 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** — **wall time: 1.18s** + +````` +(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** — **wall time: 1.16s** + +````` +(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.* + +**wall time: 1.19s** + +````` +{"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}}, +… [8745 more display lines; full output is 1215883 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 4368 +````` + +## `./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.* + +**wall time: 2.43s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [420 more display lines; full output is 29104 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: + +````` + 158572 /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.* + +**wall time: 1.60s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (12161861 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (28205873 bytes, rich family) +````` + +Artifact written: + +````` + 12161861 /aux/ci_index.lean.ripwirecache + 28205873 /aux/ci_index.rich.ripwirecache + 40367734 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 + 50934 /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** — **wall time: 1.31s** + +````` + + + + + + + + + + + + +````` + +## `./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: 3.80s** + +````` +ripwire situational-awareness — 1 changed file(s), 14 symbols in them +root: . +at: 3840b6cb8+dirty + [1] blast radius: 106 symbols across 30 files transitively depend on these changes (showing 8 of 30 files; --pr-context's own per-file blast-radius list is also capped, at 20 — shown=8 total=30 capped=1; next: --situ --limit=30) + counts_floor=1: every count above is a FLOOR, never a total (call edges are name-based; dynamic dispatch, callbacks and macros can be missing) — read a zero as "none found", never as "none exists"; graph_ambiguous=7818 graph_unresolved=4873 is the whole graph's resolver gauge (calls split … [line truncated: 301 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) — evidence order: [changed] you edited it, [partner] named after a changed file, then hops (1 = calls a changed symbol directly): + 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) + (670 test/*.sh gates are NOT modelled: script-to-binary edges are not call edges, so they never appear here — a path count, not every one invokes the binary) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="2375": + (none — 2375 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** — **wall time: 1.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 13.30s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60 more display lines; full output is 15666 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: 4.98s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60 more display lines; full output is 9805 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: 4.46s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":78,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":24,"api-new-surface":2,"at":"3840b6cb8+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:2986"}, +{"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"}, +… [57 more display lines; full output is 9675 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: 3.15s** + +````` +(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: 3.28s** + +````` +(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: 4.90s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [59 more display lines; full output is 15953 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.* + +**wall time: 1.91s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [79 more display lines; full output is 11079 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.* + +**wall time: 1.44s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [310 more display lines; full output is 18708 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.* + +**wall time: 1.99s** + +````` + + +

+

+ +````` + +## `./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: 5.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [62 more display lines; full output is 16946 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: 2.43s** + +````` +(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-13 keep this branch-free — it sits inside the PageRank sort comparator 3840b6cb80b9283d0c4147e7c268791f20388dda lane/sc-legend +````` + +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-13 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort 3840b6cb80b9283d0c4147e7c268791f20388dda lane/sc-legend +````` + +## `./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.* + +**wall time: 1.08s** + +````` +{"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"}, +{"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"} +````` + +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).* + +**wall time: 1.60s** + +````` +{"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] +{"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"}, +{"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"} +````` + +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":7815,"graph_unresolved":4873,"graph_unindexed":218,"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.* + +**wall time: 1.30s** + +````` +{"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] +{"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}}]} +````` + +## `./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: 5.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 15402 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: 2.32s** + +````` +(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: 5.15s** + +````` +(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 17860 per-symbol rows, 18476 indexed symbols total) +````` + +Artifact written: + +````` + 3016238 .ripwire_quality_baseline +# ripwire quality baseline v5 — regenerate with --quality-baseline; do not hand-edit +head 3840b6cb80b9283d0c4147e7c268791f20388dda +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: 3.67s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [51 more display lines; full output is 12696 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: 991 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: 1627 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= 42854 (~tokens at 4 bytes/token: 10713 ) +batch desc_bytes= 1190 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +explore desc_bytes= 1016 schema_bytes= 774 required=['task'] +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'] +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'] +situational_awareness desc_bytes= 559 schema_bytes= 581 required=[] +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=2033 symbols=18474 hash=630ef19e]","_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=2033 symbols=18474 hash=630ef19e]","_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.* + +**wall time: 1.22s** + +````` +{"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":""; + +// ── ONE PRICE FOR BOTH SERVING CANDIDATES (CodeRabbit, PR #215, second round on this comparison) ───── +// THE DEFECT was not a missing addend, it was two counters. The bundle candidate was priced to the byte +// (envelope, root attributes, the unproven residue, the map, the rendered , the closing tag) by the +// arithmetic in runDefaultMap, while the file candidate was priced HERE as `wf.rawBytes + the legend` — no +// `` envelope, no root attributes, no ``, and the file's RAW bytes instead of the +// blocks that actually carry them. Measured on an 864 B file: reason= said "file 1100B" for a document that +// came out 1263 B, and inside that 163 B band the tool chose — and DISCLOSED — the whole-file form while the +// bundle it rejected was the smaller document (pre-fix binary: 1262 B served under +// reason="file 1100B < bundle 1193B"). The FIRST asymmetry in this same comparison, found one review round +// earlier, was the whole-file legend; a second occurrence of one defect class means the comparison is being +// built by hand on each side, which is the bug to fix. +// +// THE RULE APPLIED is the one src/prcontext.h states verbatim above its own estimator — "ONE estimator for +// every root, never two counters (serialize.h's standing rule)". Each candidate DESCRIBES itself as an +// ExpandServeDocument and both are priced by priceExpandServeDocument, so the comparison is symmetric by +// construction: a field one side fills and the other forgets is the only way to reintroduce the defect, and a +// third candidate is priced by the same function or not priced at all. +struct ExpandServeDocument { - char open[ 160 ]; - ExpandServeChoice c; - if( wf.complete && wf.rawBytes > budgetBytes ) - { - rw::formatTo( open, sizeof( open ), "", - wf.rawBytes, budgetBytes ); - } - else if( wf.complete && wf.rawBytes < bundleBytes ) + std::size_t payloadBytes = 0; // this mode's payload AS EMITTED: the section, or the whole file's blocks + std::size_t mapBytes = 0; // the ranked map, when this mode emits one (0 when it does not) + std::size_t rootAttrBytes = 0; // attributes only this mode carries (root=, topk_default=, a payload-priced est_tokens=) + std::size_t legendBytes = 0; // legend comments only this mode emits + double selfPriceRate = 0.0; // >0: this mode prices ITSELF on its root (est_tokens=), at this B/token rate +}; + +// The whole served document, envelope included. `disclosureBytes` is the mode=/reason= attribute pair the +// choice itself writes onto the root: it is self-referential — its digits ARE the numbers it reports — so the +// caller solves it the way pricedRootAttr solves est_tokens, and hands the settled spelling's length back in. +inline std::size_t priceExpandServeDocument( const ExpandServeDocument& doc, std::size_t sharedRootBytes, std::size_t disclosureBytes ) +{ + std::size_t total = ( sizeof( "" ) - 1 ) + sharedRootBytes + doc.rootAttrBytes + disclosureBytes + + doc.legendBytes + doc.mapBytes + doc.payloadBytes + ( sizeof( "" ) - 1 ); + if( doc.selfPriceRate > 0.0 ) { - c.serveWholeFile = true; - rw::formatTo( open, sizeof( open ), "", - wf.rawBytes, bundleBytes ); + // …through pricedRootAttr itself, not a re-derived digit count: the attribute this charges is the one + // the emission path splices, from the same fixpoint over the same byte total (serialize.h). + std::size_t estTokens = 0; + total += rw::pricedRootAttr( total, doc.selfPriceRate, 0, &estTokens ).size(); } - else if( wf.complete ) + return total; +} + +inline ExpandServeChoice chooseExpandServe( const ExpandServeDocument& bundleDoc, const ExpandServeDocument& fileDoc, + std::size_t sharedRootBytes, const rw::WholeFileRender& wf, std::size_t budgetBytes ) +{ + ExpandServeChoice c; + char open[ 200 ]; + if( !wf.complete ) { - rw::formatTo( open, sizeof( open ), "", - bundleBytes, wf.rawBytes ); + rw::formatTo( open, sizeof( open ), "" ); + c.ctxOpen = open; + return c; } - else + // The pack-budget ceiling is deliberately still read off wf.rawBytes: that question is about the FILE's own + // size against --pack-budget-bytes, not about which of two documents is cheaper to serve. No comparison is + // made on this path, so no price is claimed and none is charged. + if( wf.rawBytes > budgetBytes ) { - rw::formatTo( open, sizeof( open ), "" ); + rw::formatTo( open, sizeof( open ), "", + wf.rawBytes, budgetBytes ); + c.ctxOpen = open; + return c; + } + + // THE FIXPOINT. Each candidate carries its own mode=/reason= pair when it wins, so each is charged its own + // spelling: price, spell, re-price, at most four passes — pricedRootAttr's bound, for pricedRootAttr's + // reason. Digit counts are the only thing that can move, so it settles on pass two on every real input; + // a fourth pass that still disagrees keeps the last spelling, which is the one the document receives. + char fileOpen[ 200 ] = { 0 }; + char bundleOpen[ 200 ] = { 0 }; + std::size_t fileBytes = 0, bundleBytes = 0, fileDisclosure = 0, bundleDisclosure = 0; + for( int pass = 0; pass < 4; ++pass ) + { + fileBytes = priceExpandServeDocument( fileDoc, sharedRootBytes, fileDisclosure ); + bundleBytes = priceExpandServeDocument( bundleDoc, sharedRootBytes, bundleDisclosure ); + rw::formatTo( fileOpen, sizeof( fileOpen ), "", + fileBytes, bundleBytes ); + rw::formatTo( bundleOpen, sizeof( bundleOpen ), "", + bundleBytes, fileBytes ); + // the subtraction's precondition: both spellings begin with the literal "', so + // neither can be shorter than the envelope it is measured against (formatTo always NUL-terminates). + VERIFY( std::strlen( fileOpen ) >= ( sizeof( "" ) - 1 ) && std::strlen( bundleOpen ) >= ( sizeof( "" ) - 1 ) ); + const std::size_t nextFile = std::strlen( fileOpen ) - ( sizeof( "" ) - 1 ); + const std::size_t nextBundle = std::strlen( bundleOpen ) - ( sizeof( "" ) - 1 ); + if( nextFile == fileDisclosure && nextBundle == bundleDisclosure ) + { + break; + } + fileDisclosure = nextFile; + bundleDisclosure = nextBundle; } - c.ctxOpen = open; + // Ties still go to the bundle — the richer answer at equal cost. + c.serveWholeFile = fileBytes < bundleBytes; + c.ctxOpen = c.serveWholeFile ? fileOpen : bundleOpen; return c; } @@ -1812,11 +1891,25 @@ int runDefaultMap( const MainDispatch& d ) // guarded siblings at the ceiling verdict and the topK>0 emission gate). Same guard here: a map // that will not be emitted must not be charged, exactly like every other measureEmittedMapBytes // call site in this function. - const std::size_t bundleBytes = ( sizeof( "" ) - 1 ) + ctxRootBytesWhenNoMap + ctxUnprovenBytes // H1: the root carries it in both modes - + ( mapTopK > 0 ? measureEmittedMapBytes( mapTopK, payloadTokens ) : 0 ) - + bodiesSection.xml.size() + ( sizeof( "" ) - 1 ); wholeFile = rw::renderWholeFiles( ing, expandNodes, redactPtr, d.notesPtr, cfg.compress, mapRootArg ); // D2: shaped candidate (R-R: root-relative ) - ExpandServeChoice choice = chooseExpandServe( bundleBytes, wholeFile, cfg.packBudgetBytes ); + // BOTH CANDIDATES, DESCRIBED IN THE SAME FIELDS (CodeRabbit, PR #215 — chooseExpandServe's own header + // carries the defect this replaced). The two documents differ by exactly what these fields say they + // differ by: the bundle rides a map (when one will be emitted) and the pre-rendered ; the file + // rides its blocks, its own legend, and an est_tokens= it prices ITSELF at the BODY rate + // because every byte on that path is raw code text — the same rate and the same pricedRootAttr fixpoint + // the whole-file emission below applies. topk_default="0" and the unproven residue ride BOTH openers, so + // the first is charged to both documents and the second is passed as the shared root bytes. + const std::size_t topkDefaultBytes = exactNameExpandDefault ? ( sizeof( " topk_default=\"0\"" ) - 1 ) : 0; + ExpandServeDocument bundleDoc; + bundleDoc.payloadBytes = bodiesSection.xml.size(); + bundleDoc.mapBytes = mapTopK > 0 ? measureEmittedMapBytes( mapTopK, payloadTokens ) : 0; + bundleDoc.rootAttrBytes = ctxRootBytesWhenNoMap + topkDefaultBytes; // root= and est_tokens= only where no map carries them + ExpandServeDocument fileDoc; + fileDoc.payloadBytes = wholeFile.xml.size(); // as EMITTED, not the raw file bytes + fileDoc.rootAttrBytes = ctxRootAttr.size() + topkDefaultBytes; // whole-file mode always carries root= (no rides with it) + fileDoc.legendBytes = kExpandWholeFileLegend.size(); + fileDoc.selfPriceRate = rw::kBytesPerTokenBody; + ExpandServeChoice choice = chooseExpandServe( bundleDoc, fileDoc, ctxUnprovenBytes, wholeFile, cfg.packBudgetBytes ); serveWholeFile = choice.serveWholeFile; ctxOpenStr = std::move( choice.ctxOpen ); if( exactNameExpandDefault ) @@ -1846,6 +1939,15 @@ int runDefaultMap( const MainDispatch& d ) ctxOpenStr += ctxUnprovenLegend; } + // PR #215 review item 9: the whole-file serving prints anchor rows inside , and + // carried NO legend at all — sc= (and id= before it) was an undefined first-screen attribute on the one + // --expand shape that has no other legend to read. One clause, on the shape that emits the rows. No "--" + // anywhere: it rides inside an XML comment, where a double hyphen is ill-formed (G4). + if( serveWholeFile ) + { + ctxOpenStr += kExpandWholeFileLegend; // …the SAME bytes chooseExpandServe charged the file candidate + } + // r27-emitters T2: the ride-along map. A bare `--expand=SYM` costs ~24 KB for a ~1.4 KB body because the // 200-symbol default map is emitted alongside it, and nothing ever said so. The M6 auto-selection above // now drops the whole bundle when the FILE is cheaper; when the bundle (map included) IS the cheaper diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 9ef2fbdd4..22e9a8721 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -333,7 +333,7 @@ inline constexpr McpValueSpec kMcpValueFields[] = { // DESCRIPTIONS: mcpmanifestcheck's own registered rule is that the ceiling moves for a declared // argument's obliged description and never for prose, and prose there would cost ~680 B against 159 B // of headroom. The refusal example names the NON-default value, which is the one a caller has to type. - { "legend", "a STRING legend posture: compact (the default) or full", "legend=\"full\"" }, + { "legend", "a STRING legend posture: compact (the default) or full (restores the full legend)", "legend=\"full\"" }, // ── boolean ── // F-R1-07 (2026-09-10 audit): the CLI's own answer to a route MIS-FIRE is to re-run with --no-route, // and the MCP surface had no equivalent — an agent that reads route= and disagrees was told WHICH ranker diff --git a/src/mcpverbs.h b/src/mcpverbs.h index c30c74289..f62936ba4 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1686,7 +1686,10 @@ inline std::string forTaskText( const std::string& root, const std::string& task if( page.limit > 0 || page.offset > 0 ) { const ForFilePage filePage = computeForFilePage( ing, lensRank, mcpEvidence ); - const std::string pageRootOpen = ctxRootOpen( task, noRoute ? std::string() : ( "routed: " + rc.reason + shapeDemotionNote( shape ) ), mcpRootArg ); + // PR #215 review item 4: this page composed "routed: " + rc.reason by hand and so answered in a spelling + // row 6 retired everywhere else — a parity break with the CLI page AND with this server's own bundle two + // functions down. ONE producer (filter.h routeNoteOf), same call as every other site. + const std::string pageRootOpen = ctxRootOpen( task, routeNoteOf( rc, shape, noRoute ), mcpRootArg ); return renderForFilePageXml( ing, filePage, ForPageRenderParts{ task, pageRootOpen, forCoveragePct( mcpEvidence, topLensId( lensRank ) ), page.limit, page.offset, mcpRootArg, /*compactLegend=*/false } ); } @@ -1770,7 +1773,7 @@ inline std::string forTaskText( const std::string& root, const std::string& task // §L10b + verify-wave2 F6: same trim as the CLI --for twin (verbs_for.h) — no leading " [" and no // trailing "]"; the value lands only in route=, where the attribute quote is the delimiter. const std::string mcpForAtAttrStr = gitstamp::atAttr( root ); // M10's at=, computed once: spliced onto the root AND exempted from the sigs charge below - std::string rootOpenStr = ctxRootOpen( task, noRoute ? std::string() : ( "routed: " + rc.reason + shapeDemotionNote( shape ) ), + std::string rootOpenStr = ctxRootOpen( task, routeNoteOf( rc, shape, noRoute ), // row 6: the route CODE, ONE producer (filter.h) flRootArg ); // §B1.7: same root attrs as the CLI twin (no route= under no_route, as --no-route) if( !rootOpenStr.empty() && rootOpenStr.back() == '>' ) { @@ -1814,10 +1817,33 @@ inline std::string forTaskText( const std::string& root, const std::string& task // the splice at the end of this function. rootOpenStr.insert( rootOpenStr.size() - 1, " lens=\"churn,amp,tested\"" ); } + // Read off the BUILT root open, never re-derived from noRoute: the two must agree, and only one of them is + // what the caller actually receives. + const bool mcpForRouteAttrOn = rootOpenStr.find( " route=\"" ) != std::string::npos; + // PRESENT-ONLY, ON BOTH DIALECTS (CodeRabbit, PR #215, second round). The CLI lens made both droppable + // readings present-only — sc= when a row this bundle could serve carries a scope, route= when the root + // carries the attribute — while this twin appended kForIdRouteLegend UNCONDITIONALLY, so a scope-free answer + // DEFINED an attribute that no row carried; and the exemption ledger below hand-built the same decision a + // second time, which is the four-sites-one-rule drift rw::forIdRouteLegendParts exists to close. Same rule + // and the same deliberate OVER-approximation as the CLI twin (verbs_for.h forScPresent): read off the RANKED + // SET, before the header is built, because the header built here is the one this dialect serves — the trim + // ladder may still drop the only scoped row, and a reading with nothing to define costs 29 B while the + // reverse costs a reader an attribute with no definition anywhere in the document. + bool mcpForScPresent = false; + for( std::size_t i = 0; i < ing.symbols.size() && !mcpForScPresent; ++i ) + { + mcpForScPresent = lensRank[i] > 0 && rw::hasScopeAttr( ing.symbols[i] ); + } + // ONE decision, read twice below: appended into the header here, subtracted from the sigs charge there. + const rw::ForIdRouteLegendParts mcpIdRouteParts = rw::forIdRouteLegendParts( /*legendOn=*/true, mcpForScPresent, mcpForRouteAttrOn ); std::string headerStr = rootOpenStr + "" - : ""; + ? "" + : ""; // EXTENT HONESTY (src/extentsuspect.h): how many definitions carry extent_suspect= corpus-wide — the header's // extent_suspect_syms= — and the row + header readings, appended ONLY when that is non-zero, so a corpus with // nothing flagged keeps every byte of this legend. @@ -2492,16 +2585,17 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto const Symbol& s = ing.symbols[id]; const std::uint32_t out = outOff[id + 1] - outOff[id]; w.write( " above stripped, so one row's p= and id= - // can never disagree about how this file is spelled. - const std::string canon = canonicalIdForEmit( ing, s, rootArg ); - if( canon != s.name ) { w.write( " id=\"" ); w.write( escapeXml( canon, esc ) ); w.write( "\"" ); } + w.write( "\" n=\"" ); w.write( escapeXml( s.name, esc ) ); w.write( "\"" ); // close n="…" here so sc= can follow + + // S6-C / row 6 (2026-09-12): the SHORT id. The canonical SCIP-style id is `path::scope::name`, and + // on a map row the path is the enclosing verbatim — 942 of 942 scoped rows on this tree + // repeated it, 11.2% of a flagless map. The row now prints ONLY the segment the wrapper does not + // carry: sc= the enclosing scope. The legend states the composition (id = p::sc::n), the selectors + // keep accepting the composed spelling, and test/scroundtripcheck.sh proves the composed multiset is + // byte-identical to the id= multiset this row used to print. Emitted ONLY when a scope exists — + // exactly when the canonical id differed from the bare name (canonicalId degrades to the name on an + // empty scope), so the row set that carries an identity attribute is unchanged. + writeScopeAttr( w, s, esc ); w.write( overloadsAttr( rows.overloads[i] ) ); // see overloadsAttr() above — empty in the common case @@ -3335,20 +3429,6 @@ inline void appendJsonMetricFields( std::string& out, const Symbol& s, NodeId id { rw::formatTo( num, sizeof( num ), ",\"in\":{}", ( *fanIn )[ id ] ); out += num; } } -// P2.3 — the canonical `path::scope::name` id, but ONLY when it ADDS an enclosing scope: a free function's -// canonical id IS its bare name, so repeating it would cost tokens and disambiguate nothing. "" ⇒ emit no -// id= / "id" at all. ONE definition of the rule, shared by the XML and JSON signature-row writers below and -// matching the default map's convention exactly. -// R-R: `root` is the run's root argument (empty on a multi-root run — see canonicalIdForEmit). It is -// REQUIRED rather than defaulted on purpose: a defaulted root is exactly how the four emitters below came -// to disagree about whether their id= carried the checkout prefix, and a missing argument should be a -// compile error, not a silently absolute row. -inline std::string scopedCanonicalId( const IngestResult& ing, const Symbol& s, std::string_view root ) -{ - VERIFY( s.fileId < ing.files.size() ); - std::string canon = canonicalIdForEmit( ing, s, root ); - return canon == s.name ? std::string{} : canon; -} // P2.3/P2.4 — the per-row descriptive facts sigRowHead() folds in, grouped (not individual params) so the // helper stays well under the params-regression bar. `lens` is the pre-rendered churn/amp/clone/tested attr @@ -3382,10 +3462,11 @@ inline std::string lensRowPath( const IngestResult& ing, std::uint32_t fileId, s // P2.3/P2.4 — the exact "" opening tag of ONE signature row, defined once so the two-phase (globally // budgeted) emitter and the streaming emitter can never drift by a byte: the budget ledger measures exactly // the string this returns. -// P2.3 — n= (and id= when the canonical `path::scope::name` ADDS an enclosing scope; a free function's -// canonical id IS its bare name, so it costs zero bytes there) is the CHAIN KEY: without it a reader had to -// parse a C++ declarator out of the signature text to chain into --expand/--callers. Same canonicalId form -// the default map's uses, so an id read out of a bundle addresses the same symbol in either lens. +// P2.3 — n= (and sc= when the symbol has an enclosing scope; a free function's canonical id IS its bare +// name, so it costs zero bytes there) is the CHAIN KEY: without it a reader had to parse a C++ declarator +// out of the signature text to chain into --expand/--callers. Row 6: the id composes as p::sc::n — the same +// rule the default map's rows follow, so an id composed from a bundle row addresses the same +// symbol in either lens. // The `l=` prefix is DELIBERATELY kept first — existing consumers key on the "'s) supplies the rest + { head += " sc=\""; head += escapeXml( s.scope, esc ); head += "\""; } // P7 (terminality round A, lane R, 2026-09-05): p= (and layer= when the file sits in a builtin layer) ride // EVERY row that carries r= — the lens serving is FLAT now (rows in rank order, no wrapper), so the // row itself names its file; the non-lens serving (rank 0: --pack-signatures) keeps the wrapper and no p=. @@ -4642,24 +4723,82 @@ inline std::vector calleeWalkOrder( NodeId id, const std::vector` — the names-only row. No file read, no signature slice, and no redaction -// seam: a bare identifier is not a credential shape, which is why this row does not take a RedactCounts -// the way the signature row below does. Charged at what it actually emits. -inline void appendCalleeNameRow( std::string& callsBody, const Symbol& cs, std::vector& esc, - std::size_t& used, const CalleeCallsSink& sink ) -{ - char nb[ 32 ]; - rw::formatTo( nb, sizeof( nb ), "\" l=\"{}\"/>", cs.line ); - callsBody += "`), COLLECTED rather than written: row 6 (2026-09-12) +// merges the same-named callees of ONE block into one row whose l= comma-joins their definition lines +// (`` — two overloads, or a declaration and its definition, that used to cost a +// full row each: 716 B over the twelve --for answers of the 2026-09-12 re-measure). Walk order is kept: +// a merged row sits where its FIRST callee sat. shown= still counts callees, never rows — the legend says +// so. No file read, no signature slice, and no redaction seam: a bare identifier is not a credential shape, +// which is why this path does not take a RedactCounts the way the signature row below does. +// +// CHARGED AT WHAT IT PRINTS (PR #215 review item 8). This used to charge every callee `name + 16` whether it +// opened a row or merged into one, on the reasoning that "the merge only ever saves bytes past that charge". +// It does not: the charge is what the BLOCK'S CAP spends, so a block of overloads was billed a full row for +// each `,203` it actually printed — about 4 B charged as 20-30 — and the cap then fired early and wrote +// `capped="1"` over a listing that would have fit whole. A cap that cuts an answer it did not need to cut is +// the class METHODOLOGY §9 forbids outright: the disclosure is honest about a cut that should never have +// happened. A merge is charged the comma and the digits it appends, and nothing else. +// +// l= IS ASCENDING (same item). The list was appended in WALK order, which is the lens's RANK order, so the two +// definition lines of one overloaded name came out `l="70,69"` on one query and `l="69,70"` on another — the +// same fact in two spellings, from a document that promises determinism. Line numbers have a natural order and +// it is not the ranker's; they are sorted ascending, so a row's content depends on the row and not on how the +// walk reached it. Row ORDER is unchanged: a merged row still sits where its first callee sat. +struct MergedCalleeNameRow +{ + std::string_view name; // a view into ing.symbols — stable for the emitter's lifetime + std::vector lines; // every definition line of that name; joined ascending at append time +}; + +inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, + std::size_t& used, const CalleeCallsSink& sink ) +{ + char lb[ 16 ]; + rw::formatTo( lb, sizeof( lb ), "{}", cs.line ); + bool merged = false; + for( MergedCalleeNameRow& r : rows ) + { + if( r.name == cs.name ) + { + r.lines.push_back( cs.line ); merged = true; + break; + } + } + if( !merged ) + { + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line } } ); + } + // the comma and the digits a merge appends, or the whole row it opens + used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16; if( sink.recorded ) { sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, std::string() } ); // §H5: no sig to record } } +// …and the rows written out, once the block's walk is complete. +inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& rows, std::vector& esc ) +{ + for( MergedCalleeNameRow& r : rows ) + { + std::sort( r.lines.begin(), r.lines.end() ); // ascending, so the row reads the same whatever the walk order was + callsBody += " 0 ) + { + callsBody += ','; + } + char lb[ 16 ]; + rw::formatTo( lb, sizeof( lb ), "{}", r.lines[i] ); + callsBody += lb; + } + callsBody += "\"/>"; + } +} + // §P10.1: the disclosed block // for one body's 1-hop callee signatures — extracted out of packBodies so the disclosure logic doesn't // inflate packBodies' own complexity/LOC. `total` is outOff[id+1]-outOff[id] — outTargets is deduped-per- @@ -4686,8 +4825,9 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector const std::vector walk = calleeWalkOrder( id, outOff, outTargets, sink ); // see it for the order - std::string callsBody; - int shown = 0; + std::string callsBody; + std::vector nameRows; // names-only rendering: collected, merged by name, written after the walk + int shown = 0; for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < 16 && used < budgetBytes; ++k ) { const NodeId cid = walk[ k - outOff[id] ]; @@ -4697,10 +4837,10 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector } const Symbol& cs = ing.symbols[cid]; - // COMPACT: the names-only rendering — see appendCalleeNameRow above for what it does and does not do. + // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. if( sink.namesOnly ) { - appendCalleeNameRow( callsBody, cs, esc, used, sink ); + collectCalleeNameRow( nameRows, cs, used, sink ); ++shown; continue; } @@ -4725,6 +4865,7 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, sig } ); // §H5 } } + appendMergedCalleeNameRows( callsBody, nameRows, esc ); // no-op on the signature rendering (nameRows stays empty) appendCallsBlock( out, total, shown, callsBody ); } @@ -5447,9 +5588,16 @@ inline WholeFileRender renderWholeFiles( const IngestResult& ing, const std::vec r.rawBytes += body.size(); // sym= anchors (name:line per requested node in this file, request order) + their field notes, - // plus (D2) an row per symbol whose canonical id adds an enclosing scope — the + // plus (D2) an row per symbol whose canonical id adds an enclosing scope — the // exact S6-C emit-only-when-disambiguating rule the map rows follow, so the canonical-id surface // survives the serving-mode flip at zero cost for scope-less symbols. + // + // ROW 6 REACHED HERE LAST (PR #215 review item 9). These rows kept the full `id="PATH::SCOPE::NAME"` + // on the argument that "their path does not repeat on the row" — but it does: the row sits inside + // , which has just printed it, exactly like the wrapper the map dropped id= for. + // And this document carried NO LEGEND AT ALL, so id= was an undefined first-screen attribute on top of + // being a repetition. sc= here, one presence rule (hasScopeAttr) with every other emitter, and the + // whole-file root now states the composition. Gate: test/scroundtripcheck.sh (E). std::string anchors; std::string anchorRows; std::string noteStr; @@ -5467,13 +5615,12 @@ inline WholeFileRender renderWholeFiles( const IngestResult& ing, const std::vec anchors += s.name; anchors += ':'; anchors += std::to_string( s.line ); - const std::string canon = canonicalIdForEmit( ing, s, rootArg ); // R-R - if( canon != s.name ) + if( hasScopeAttr( s ) ) { anchorRows += ""; @@ -7190,8 +7337,7 @@ inline void serializeJson( std::FILE* out, const IngestResult& ing, const std::v w.write( "{\"t\":" ); writeJsonStr( w, symTag( s.kind ), esc ); w.write( ",\"n\":" ); writeJsonStr( w, s.name, esc ); - const std::string canon = canonicalIdForEmit( ing, s, rootArg ); // R-R: matches the XML sibling - if( canon != s.name ) { w.write( ",\"id\":" ); writeJsonStr( w, canon, esc ); } + if( hasScopeAttr( s ) ) { w.write( ",\"sc\":" ); writeJsonStr( w, s.scope, esc ); } // row 6: the XML sibling's sc=, one presence rule (hasScopeAttr) if( rows.overloads[ rowIndex ] > 1 ) { rw::formatTo( num, sizeof( num ), ",\"overloads\":{}", rows.overloads[ rowIndex ] ); w.write( num ); } @@ -7407,11 +7553,11 @@ inline std::string jsonSigRowHead( const IngestResult& ing, NodeId id, std::uint rw::formatTo( num, sizeof( num ), "{{\"l\":{}", s.line ); head += num; // P2.3: the chain key — "n" always, "id" only when the canonical form adds an enclosing scope - // (the XML sibling's rule, scopedCanonicalId above), so a JSON consumer can chain onward too. + // (the XML sibling's rule, sigRowHead above), so a JSON consumer can chain onward too. appendJsonStrField( head, ",\"n\":", s.name ); - if( const std::string canon = scopedCanonicalId( ing, s, rootArg ); !canon.empty() ) + if( hasScopeAttr( s ) ) // row 6: the XML sibling's sc=, ONE presence rule — keys mirror attribute names one to one { - appendJsonStrField( head, ",\"id\":", canon ); + appendJsonStrField( head, ",\"sc\":", s.scope ); } // P7: the row names its file (and its builtin layer) — the XML sibling's p=/layer=, same root-relative spelling appendJsonStrField( head, ",\"p\":", lensRowPath( ing, fileId, rootArg ) ); diff --git a/src/taskroute.h b/src/taskroute.h index 5f512d007..c205c8151 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -17,6 +17,7 @@ #include "query.h" // isKnownLayerWord — the layer vocabulary --verify enforces at evaluation #include "sarif.h" // rootRelativeUri / rootPrefixOf — the ONE root-relative path rule the map emits with #include "verify.h" // parseClaim — the SHIPPED claim grammar; the router never re-implements it +#include "compactlegend.h" // rw::legendCompactAppliesTo — ONE answer to "does --legend=compact apply here" namespace rw::taskroute { @@ -690,7 +691,7 @@ inline std::optional instrumentedTaskChoice( std::string_view task, if( !quoted.empty() ) { return RouteChoice{ "grep-handles", "ripwire-mcp", "safe-edit handle wording plus a quoted literal to anchor them", - commandWithValue( root, "--grep=", quoted ) + " --handles", 100, 88 }; + commandWithValue( root, "--grep=", quoted ) + " --handles --legend=compact", 100, 88 }; } } if( has( lower, "compact legend" ) || ( has( lower, "legend" ) && has( lower, "compact" ) ) ) @@ -702,13 +703,13 @@ inline std::optional instrumentedTaskChoice( std::string_view task, && ( has( lower, "doctor" ) || has( lower, "integration" ) || has( lower, "wired" ) || has( lower, "set up" ) || has( lower, "setup" ) ) ) { return RouteChoice{ "codex-doctor", "ripwire-mcp", "codex plus integration/health wording", - ripRoot + "--doctor --agent=codex", 100, 86 }; + ripRoot + "--doctor --agent=codex --legend=compact", 100, 86 }; } if( ( has( lower, "shell gate" ) || has( lower, "test gate" ) || has( lower, "test-gate" ) ) && ( has( lower, "evidence" ) || has( lower, "why" ) || has( lower, "which" ) || has( lower, "picked" ) || has( lower, "chose" ) ) ) { return RouteChoice{ "gate-evidence", "ripwire-change-check", "shell-gate selection asked for by its evidence", - ripRoot + "--test-gate", 100, 85 }; + ripRoot + "--test-gate --legend=compact", 100, 85 }; } return std::nullopt; } @@ -728,7 +729,7 @@ inline std::optional flowTaskChoice( std::string_view task, std::st if( !fileLine.empty() ) { return RouteChoice{ "at-line", "ripwire-navigate", "a file:line location named in the task", - commandWithValue( root, "--slice=", "@" + fileLine ), 100, 84 }; + commandWithValue( root, "--slice=", "@" + fileLine ) + " --legend=compact", 100, 84 }; } // who-writes: "who writes/sets/modifies/assigns SYM" needs exactly one resolved symbol, the same // discipline edit-contract uses. --uses=SYM is the closest shipped surface (every resolvable @@ -744,7 +745,7 @@ inline std::optional flowTaskChoice( std::string_view task, std::st { return RouteChoice{ "who-writes", "ripwire-navigate", "one exact indexed symbol plus writer-attribution wording (Owner.field coupling deferred)", - commandWithValue( root, "--uses=", symbols[0] ), 100, 81 }; + commandWithValue( root, "--uses=", symbols[0] ) + " --legend=compact", 100, 81 }; } // data-flow: "where does this value come from" / "which statements feed X" / "trace the flow of X" // needs exactly one resolved symbol too — --slice needs a SYM to pick the one definition to slice. @@ -765,11 +766,11 @@ inline std::optional flowTaskChoice( std::string_view task, std::st { return RouteChoice{ "data-flow", "ripwire-navigate", "one exact indexed symbol plus data-flow wording plus a variable-slot mention", - commandWithValue( root, "--slice=", symbols[0] + ":" + variable ) + " --slice-flow=back", 100, 83 }; + commandWithValue( root, "--slice=", symbols[0] + ":" + variable ) + " --slice-flow=back --legend=compact", 100, 83 }; } return RouteChoice{ "data-flow", "ripwire-navigate", "one exact indexed symbol plus data-flow wording (no variable named — lists its locals)", - commandWithValue( root, "--slice=", symbols[0] ), 100, 83 }; + commandWithValue( root, "--slice=", symbols[0] ) + " --legend=compact", 100, 83 }; } return std::nullopt; } @@ -802,7 +803,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( handoffScore >= 7 ) { return RouteChoice{ "handoff-brief", "ripwire-handoff", "briefing a SECOND party (successor/teammate/next session)", - ripRoot + "--handoff", 100, 79 }; + ripRoot + "--handoff --legend=compact", 100, 79 }; } // plan-lint: a PLAN/DESIGN file's structure. Value-carrying — the verb refuses a file that is not // there, so the route fires only when the task names one. @@ -818,7 +819,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( !planDoc.empty() ) { return RouteChoice{ "plan-lint", "ripwire-before-you-build", "plan/design structure wording plus a named markdown file", - commandWithValue( root, "--plan-lint=", planDoc ), 100, 78 }; + commandWithValue( root, "--plan-lint=", planDoc ) + " --legend=compact", 100, 78 }; } } // trace-prose: the user SAYS they are holding a trace instead of pasting one. looksLikeTrace matches a @@ -832,7 +833,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: || has( lower, "translate" ) || has( lower, "i have" ) || has( lower, "here is" ) || has( lower, "frames" ) ) ) { return RouteChoice{ "trace-prose", "ripwire-find-bug", "a trace/report described rather than pasted; pass it on stdin", - ripRoot + "--from-trace=-", 100, 77 }; + ripRoot + "--from-trace=- --legend=compact", 100, 77 }; } // security-scan: vetting something UNTRUSTED before installing it. --scan-skills defaults its directory, // so the valueless form is a real command; a named file upgrades it to --scan-skill=FILE. @@ -845,9 +846,9 @@ inline std::optional catalogTaskChoice( std::string_view task, std: const std::string skillFile = firstPathTokenWithSuffix( task, { ".md", ".markdown", ".json", ".sh" } ); return skillFile.empty() ? RouteChoice{ "scan-skills", "ripwire-security-scan", "pre-install vetting wording; --scan-skills defaults its directory", - ripRoot + "--scan-skills", 100, 76 } + ripRoot + "--scan-skills --legend=compact", 100, 76 } : RouteChoice{ "scan-skill", "ripwire-security-scan", "pre-install vetting wording plus a named file", - commandWithValue( root, "--scan-skill=", skillFile ), 100, 76 }; + commandWithValue( root, "--scan-skill=", skillFile ) + " --legend=compact", 100, 76 }; } // opt-remarks: clang optimization remarks while building ripwire itself. The ONLY skill with no verb of // its own — the ranked lens is what finds the symbol a remark names, and the reason says exactly that @@ -868,7 +869,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( layersScore >= 8 ) { return RouteChoice{ "architecture-health", "ripwire-layers", "architecture-health wording (--arch=FILE is the gating form and needs a rules file)", - ripRoot + "--deps", 100, 74 }; + ripRoot + "--deps --legend=compact", 100, 74 }; } // quality-bar: what YOU just wrote, before you call it done. Deliberately narrow so it cannot steal the // dirty-worktree review route below, whose wording is about a DIFF and a push rather than about debt. @@ -883,7 +884,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( qualityScore >= 9 ) { return RouteChoice{ "quality-check", "ripwire-quality-bar", "own-code quality wording (what got WORSE), not merge safety", - ripRoot + "--quality-delta", 100, 73 }; + ripRoot + "--quality-delta --legend=compact", 100, 73 }; } // perf-target: a MEASURED profile that NAMES a symbol. Static metrics are not runtime heat, so this // needs the profile wording AND the one symbol the profile named — never the wording alone. @@ -894,7 +895,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( symbols.size() == 1 && perfScore >= 7 ) { return RouteChoice{ "perf-symbol", "ripwire-perf-target", "a measured profile plus the one symbol it names", - commandWithValue( root, "--around=", symbols[0] ), 100, 72 }; + commandWithValue( root, "--around=", symbols[0] ) + " --legend=compact", 100, 72 }; } // graph-query: a closure question the fixed verbs cannot phrase. The EXPRESSION is built only out of // what the task supplied — the symbol it named and the direction it asked for — and the depth is a @@ -907,7 +908,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: const bool outward = has( lower, "reachable from" ) || has( lower, "everything it calls" ) || has( lower, "downstream of" ); const std::string expr = std::string( outward ? "callees(name(\"" : "callers(name(\"" ) + symbols[0] + "\"),3)"; return RouteChoice{ "graph-query", "ripwire-graph-query", "a bounded-closure question plus one named symbol (depth 3 is the default; raise it)", - commandWithValue( root, "--graph-query=", expr ), 100, 71 }; + commandWithValue( root, "--graph-query=", expr ) + " --legend=compact", 100, 71 }; } // fresh-eyes: maintenance risk in code the speaker did NOT write. LAST of the catalog tier because its // vocabulary is the broadest, so every more specific reading above gets first refusal. @@ -918,7 +919,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: if( riskScore >= 8 ) { return RouteChoice{ "maintenance-risk", "ripwire-fresh-eyes", "maintenance-risk wording about code the speaker did not write", - ripRoot + "--hotspots", 100, 70 }; + ripRoot + "--hotspots --legend=compact", 100, 70 }; } return std::nullopt; } @@ -1151,7 +1152,7 @@ inline std::optional directTaskChoice( std::string_view task, std:: if( !quoted.empty() ) { return RouteChoice{ "exact-grep", "ripwire-navigate", "quoted literal plus exact-search wording", - commandWithValue( root, "--grep=", quoted ) + " --grep-context=2 --limit=40", 100, 85 }; + commandWithValue( root, "--grep=", quoted ) + " --grep-context=2 --limit=40 --legend=compact", 100, 85 }; } const int postEditScore = phraseScore( lower, { { "just edited", 9 }, { "just finished editing", 9 }, { "my edit to", 9 }, { "changed its signature", 9 }, @@ -1166,7 +1167,7 @@ inline std::optional directTaskChoice( std::string_view task, std:: { return RouteChoice{ "edit-contract", "ripwire-change-check", "one exact indexed symbol plus post-edit contract wording", - commandWithValue( root, "--edit-check=", symbols[0] ), 100, 82 }; + commandWithValue( root, "--edit-check=", symbols[0] ) + " --legend=compact", 100, 82 }; } // at-line / who-writes / data-flow: structural or phrase-scored the same way the categories above // are, just extracted into their own function (see flowTaskChoice's own comment) to keep this ladder @@ -1185,8 +1186,40 @@ inline std::optional directTaskChoice( std::string_view task, std:: return std::nullopt; } +// ONE PLACE APPLIES THE COMPACT-LEGEND POSTURE (PR #215 review item 5). A1-2 put --legend=compact on 26 route +// commands by editing 26 strings, which is 26 chances to miss one and no rule for the 27th. classifyRoutes below +// is the whole router; classify() is the one exit, and it applies the posture to every choice it returns — which +// is what makes the 27th free: #218's recency intent (merged here) spells its commands without the flag and +// gets it anyway, without knowing the rule exists. +// +// WHAT DECIDES: rw::legendCompactAppliesTo (compactlegend.h), the SAME list of non-XML surfaces cli.h REFUSES the flag +// on, asked of a command string instead of a parsed Config. So the router cannot generate a command its own +// binary rejects — which it did: `--zoom --legend=compact --mermaid` shipped in a skill, and a hand-listed gate +// enforced it. --for is exempt by policy, not by refusal, and legendCompactAppliesTo says so in one place. +// Idempotent: a command that already carries --legend= is left alone, so the hand-applied ones are untouched and +// this is a no-op on them. Gate: test/taskroutecheck.sh runs every generated command against the binary. +// +// caps rides through untouched (#218): classify() decides nothing about routing, it only applies the posture to +// what classifyRoutes returned, so every routing input is forwarded verbatim. +inline TaskRouteResult classifyRoutes( std::string_view task, const std::string& root, const IngestResult& ing, bool git, bool dirty, + const RouterCaps& caps ); + inline TaskRouteResult classify( std::string_view task, const std::string& root, const IngestResult& ing, bool git, bool dirty, const RouterCaps& caps = {} ) +{ + TaskRouteResult result = classifyRoutes( task, root, ing, git, dirty, caps ); + for( RouteChoice& choice : result.choices ) + { + if( rw::legendCompactAppliesTo( choice.command ) ) + { + choice.command += " --legend=compact"; + } + } + return result; +} + +inline TaskRouteResult classifyRoutes( std::string_view task, const std::string& root, const IngestResult& ing, bool git, bool dirty, + const RouterCaps& caps ) { TaskRouteResult result; result.facts.git = git; @@ -1202,7 +1235,7 @@ inline TaskRouteResult classify( std::string_view task, const std::string& root, result.score = 100; result.margin = 100; result.choices.push_back( { "verify-claim", "ripwire-navigate", "closed claim grammar", - commandWithValue( root, "--verify=", task ), 100, 100 } ); + commandWithValue( root, "--verify=", task ) + " --legend=compact", 100, 100 } ); return result; } if( result.facts.trace ) @@ -1210,7 +1243,7 @@ inline TaskRouteResult classify( std::string_view task, const std::string& root, result.status = RouteStatus::Recommend; result.score = result.margin = 100; result.choices.push_back( { "trace-debug", "ripwire-find-bug", "stack-trace shape; pass the trace on stdin", - "ripwire " + shSingleQuote( root ) + " --from-trace=-", 100, 90 } ); + "ripwire " + shSingleQuote( root ) + " --from-trace=- --legend=compact", 100, 90 } ); return result; } if( std::optional direct = directTaskChoice( task, lower, root, result.facts.resolvedSymbols ) ) @@ -1225,7 +1258,7 @@ inline TaskRouteResult classify( std::string_view task, const std::string& root, result.status = RouteStatus::Recommend; result.score = result.margin = 100; result.choices.push_back( { "connect-symbols", "ripwire-navigate", "three or more exact indexed symbols", - commandWithValue( root, "--connect=", commaSymbols( result.facts.resolvedSymbols ) ), 100, 80 } ); + commandWithValue( root, "--connect=", commaSymbols( result.facts.resolvedSymbols ) ) + " --legend=compact", 100, 80 } ); return result; } if( result.facts.resolvedSymbols.size() == 1 && ( has( lower, "understand" ) || has( lower, "implementation" ) || has( lower, "how does" ) ) ) @@ -1233,7 +1266,7 @@ inline TaskRouteResult classify( std::string_view task, const std::string& root, result.status = RouteStatus::Recommend; result.score = result.margin = 100; result.choices.push_back( { "understand-symbol", "ripwire-navigate", "one exact indexed symbol plus understand wording", - commandWithValue( root, "--expand=", result.facts.resolvedSymbols[0] ), 100, 70 } ); + commandWithValue( root, "--expand=", result.facts.resolvedSymbols[0] ) + " --legend=compact", 100, 70 } ); return result; } @@ -1249,18 +1282,18 @@ inline TaskRouteResult classify( std::string_view task, const std::string& root, const int planScore = phraseScore( lower, { { "plan", 5 }, { "implementation", 2 }, { "feature", 4 }, { "scope", 4 }, { "multi-symbol", 4 }, { "before building", 4 }, { "new ", 1 } } ); addLexical( candidates, "plan-feature", "ripwire-before-you-build", "prospective feature planning wording", - commandWithValue( root, "--pack-task=", task ), planScore, 8, 50 ); + commandWithValue( root, "--pack-task=", task ) + " --legend=compact", planScore, 8, 50 ); const int reuseScore = phraseScore( lower, { { "about to write", 8 }, { "one helper", 5 }, { "one function", 5 }, { "one class", 5 }, { "helper", 3 }, { "function", 2 }, { "class", 2 } } ); addLexical( candidates, "reuse-one-symbol", "ripwire-reuse-first", "about-to-write one-symbol wording", - commandWithValue( root, "--exemplar=", task ), reuseScore, 10, 40 ); + commandWithValue( root, "--exemplar=", task ) + " --legend=compact", reuseScore, 10, 40 ); const int writeTestsScore = phraseScore( lower, { { "missing coverage", 8 }, { "no tests", 6 }, { "untested", 5 }, { "regression gate", 4 }, { "add", 3 }, { "write", 3 }, { "find", 2 }, { "cover", 2 } } ); addLexical( candidates, "write-tests", "ripwire-write-tests", "test-gap wording plus a test-writing action", - "ripwire " + shSingleQuote( root ) + " --seams", writeTestsScore, 8, 35 ); + "ripwire " + shSingleQuote( root ) + " --seams --legend=compact", writeTestsScore, 8, 35 ); const int locateScore = phraseScore( lower, { { "find the code", 8 }, { "locate", 7 }, { "responsible", 4 }, { "bug", 3 }, { "wrong output", 4 }, { "crash", 4 }, { "symptom", 3 } } ); diff --git a/src/tracelocus.h b/src/tracelocus.h index f0e82de91..3b54f2f2a 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -1043,7 +1043,7 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph // §B7.5 (CA4): the rows this verb emits carry the same ranking-row vocabulary --pack-task // spells out, and this legend defined only the frame half — a reader met cx=/ccx=/in= on the // signature rows with nothing to read them against, the identical gap on the identical rows. - h += "On a row (rows in r= order): n=name, id=canonical(when scoped), p=file, t=kind, cx=cyclomatic complexity, " + h += "On a row (rows in r= order): n=name, sc=enclosing scope (when scoped; the full id is p::sc::n), p=file, t=kind, cx=cyclomatic complexity, " "ccx=cognitive complexity, in=reuse-count (absent = not measured, never a false 0). "; h += "rank 1 = the innermost in-corpus frame; its FULL body follows, other suspects as signatures. "; h += hopLegendOf( hop ); // LB-A: empty unless the hop fired (byte-identical otherwise) @@ -1175,6 +1175,12 @@ inline FromTraceResult fromTraceBundleText( const IngestResult& ing, const Graph const CeilingLadderChoice chosen = climbCeilingLadder( [ & ]( bool, bool withSrcEcho, std::string_view extra ) { return buildTraceHeader( withSrcEcho, extra ); }, headerStr, pricedBytesOf( whole.size() ) - headerStr.size() + rootAttrsBound, + // ONE ceiling twice: this lens states its ceiling in BYTES and labels + // over_ceiling= against `namedCeiling`, so it has no token-rate + // mismatch for the two-ceiling ladder to resolve (PR #215 item 1 names + // --for and --pack-task). Passing the same predicate for both rungs + // reproduces the pre-#215 single-ceiling climb exactly. + ceilingAllowanceFromBudgetBytes( bundleBudget ), ceilingAllowanceFromBudgetBytes( bundleBudget ), /*hasRouteAttr=*/false, kNotes ); if( chosen.header != headerStr ) diff --git a/src/verbs_for.h b/src/verbs_for.h index 71a2577e7..7ef3d69fa 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -155,7 +155,7 @@ rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task // QUOTE is the delimiter here. The two stop-anchor helpers that grepped `routed: [^]]*` // (test/adaptivecheck.sh routeof(), test/routecheck.sh reasonOf()) are re-pinned to `[^"]*` in the // same commit — the quote is the real end of the value and was all along. - out.routeNote = "routed: " + rc.reason + shapeDemotionNote( shape ); + out.routeNote = routeNoteOf( rc, shape, /*noRoute=*/false ); // row 6: the code itself, no "routed: " prose prefix — ONE producer (filter.h) out.docTierTag = shapeDocTierTag( shape ); // §A4f: the machine form of the same fact, for --format=candidates out.routeTag = ( rc.which == LexMode::NameExact ) ? "name-exact" : "subtoken+body"; // §A4f: the machine form of the same fact out.anchorDefs = std::move( const_cast( rc ).anchorDefs ); // empty unless the route was DECIDED by names (lexical.h) @@ -480,6 +480,9 @@ struct ForLensHeaderParts // the facts (r= attrs, the element) survive, only the // explainer goes — the L1 "first rung that costs no unique // information" ordering. + bool idRouteLegend = true; // row 6 (2026-09-12): the sc=/route= readings (graphlegend.h + // kForIdRouteLegend) — ceiling-droppable exactly like the two + // above; rung zero clears all three and the note names them bool legendDropped = false; // …and rung zero SAYS SO: set with the two clears above, it splices // kForLegendDroppedNote. A field rather than a note the rung // appends once, because the ladder rebuilds this header up to @@ -488,6 +491,29 @@ struct ForLensHeaderParts std::string_view rootArg; // R-E (2026-08-17): the single-root run's own root= — the ladder's // route-dropped rebuild below calls ctxRootOpen a second time and // must carry the SAME root as the pre-built rootOpenStr did. + bool scPresent = true; // PR #215 review: does any row this bundle serves carry sc=? The two + // dialects' sc= readings are present-only on it, and the dropped-legend + // note names sc= only when it had something to define. Defaults TRUE — + // an over-approximation costs a reader nothing but a clause, while an + // under-approximation is an undefined first-screen attribute. + + // ── THE DROPPABLE LEGEND, as ONE bit ────────────────────────────────────────────────────────────── + // confidenceNote / tailLegend / idRouteLegend moved in lock step at every read and every write, and the + // guard that used to spell all three was true in the only state rung zero can be reached in. One question, + // asked once; one clear, spelled once. legendDropped is the disclosure and is set by the same call, so a + // future clear cannot forget it (which is exactly how a silent drop ships). + bool legendDroppable() const noexcept + { + return !confidenceNote.empty() || tailLegend || idRouteLegend; + } + void dropDroppableLegend() noexcept + { + confidenceNote = {}; + tailLegend = false; // deep-tail: the explainer falls with the confidence clause — the r= attrs + // and the element (the facts) survive + idRouteLegend = false; // row 6: the sc=/route= readings fall with them — the attributes stay + legendDropped = true; // …and this is the sentence that says so, naming exactly what fell + } }; // Splice a pre-formatted fragment in front of a structural boundary, or leave the document exactly as it @@ -547,11 +573,6 @@ inline std::string rootOpenWithSchema( std::string rootOpen, std::string_view sc using rw::ForConfidence; using rw::deriveForConfidence; -// P1 (L7): the compact dialect's confidence clause — ONE constant, because runForLens exempts its bytes from the sig -// trim by size (the full dialect exempts forConf.note the same way); a second spelling would desynchronize the ledger. -inline constexpr std::string_view kForCompactConfidenceClause = - " [confidence=/margin_pct=: the ranked head's largest relative score drop; low = flat ranking, a starting point]"; - // L1 — RUNG ZERO'S OWN DISCLOSURE. The ceiling ladder's rung zero (runForLens, below) buys header bytes back by // dropping the two clauses whose loss costs no UNIQUE information: the confidence=/margin_pct= + // budget_tokens=/max_tokens= sentence and the r=/tail sentence. Every fact they describe stays on the document; @@ -592,63 +613,204 @@ inline constexpr std::string_view kForCompactConfidenceClause = // a bundle that had fitted 3.9% past the ceiling it names. METHODOLOGY §9 is the tie-breaker: the smaller document // inside the budget beats the larger one past it. Two honest spellings were available and the shorter was taken; // that is not the same as trimming a disclosure until a pin goes green, and the day it is, the pin moves instead. -inline constexpr std::string_view kForLegendDroppedNote = - " [legend clauses: confidence=/margin_pct=, budget_tokens=/max_tokens= and r=/tail (total= shown= capped=) " - "dropped (ceiling) - the attributes stay; a wider token-budget defines them]"; -// L-W: the same note on a THIN answer, whose root also carries coverage= (present-only) — the clause that defined it -// rode the confidence sentence and fell with it, so this spelling names it too. Two constants, not one assembled at -// runtime, for the byte-ledger reason kForCompactConfidenceClause states. -inline constexpr std::string_view kForLegendDroppedNoteThin = - " [legend clauses: confidence=/margin_pct=/coverage=, budget_tokens=/max_tokens= and r=/tail (total= shown= capped=) " - "dropped (ceiling) - the attributes stay; a wider token-budget defines them]"; - -// …and the COMPACT DIALECT's spelling, which names two fewer attributes because that dialect never had them. -// The full dialect's confidence sentence carries "[budget_tokens=/max_tokens=: the token ceiling this bundle was -// shaped against]" appended to it (runForLens), so rung zero really does take those definitions down with it -// there. The compact dialect emits kForCompactConfidenceClause instead, which defines confidence= and -// margin_pct= and nothing else — budget_tokens= has never been defined in it at any budget. Telling a compact -// reader it was "dropped (ceiling)" would be the exact error this note exists to prevent, pointed the other way: -// a feature that is missing, reported as a cut. Two constants rather than one assembled at runtime, for the -// reason kForCompactConfidenceClause is one constant — the byte ledgers that exempt and charge these strings -// read their sizes, and a string built at runtime has no size to read at compile time. -inline constexpr std::string_view kForLegendDroppedNoteCompact = - " [legend clauses: confidence=/margin_pct= and r=/tail (total= shown= capped=) dropped (ceiling) - " - "the attributes stay; a wider token-budget defines them]"; -inline constexpr std::string_view kForLegendDroppedNoteCompactThin = - " [legend clauses: confidence=/margin_pct=/coverage= and r=/tail (total= shown= capped=) dropped (ceiling) - " - "the attributes stay; a wider token-budget defines them]"; - -inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, std::string_view extraNotes ) +// RUNG ZERO'S OWN DISCLOSURE, ASSEMBLED FROM WHAT ACTUALLY FELL (PR #215 review item 2). +// +// WHAT WAS WRONG. This was four constants — full/thin x default/compact — chosen by one `coverage=` lookup, and +// three of the four were wrong about the document they described: +// * the THIN spellings named neither sc= nor route=, although rung zero clears idRouteLegend on a thin answer +// exactly as it does on any other. MEASURED on the pre-fix binary: `test/cppqualfix --for='mutex and pick +// overloads' --token-budget=300` served two rows, mentioned sc= nowhere in its legend, and its note +// named neither — an attribute with no reading and no disclosure that it had lost one. +// * the non-thin default spelling named route= unconditionally, so a --no-route run was told a definition had +// been "dropped (ceiling)" for an attribute that never rode. That is the error this note exists to prevent, +// pointed the other way: a feature that is absent, reported as a cut. +// * the compact spellings named sc=, which that dialect defines in its always-on rows clause and rung zero +// does not take down. +// A note about a cut is worth nothing if it is not about THIS document, so it is built from this document: the +// four booleans are the four facts that decide what the clauses defined. +// +// ASSEMBLED AT RUNTIME, deliberately. The older comment here argued for constants because "the byte ledgers that +// exempt and charge these strings read their sizes"; no ledger does — this note is spliced into the header and +// priced by finishForLensHeader's own fixpoint, like every other late splice. Nothing reads its size at compile +// time, and the four constants bought only the drift above. +// +// No "--" anywhere: it rides inside an XML comment, where a double hyphen is ill-formed (G4). +// +// compact the compact dialect, whose confidence clause defines confidence=/margin_pct= and NOTHING else +// (budget_tokens= has never been defined there at any budget, so naming it as dropped would be +// the same lie in miniature), and whose sc= reading rides the un-droppable rows clause. +// thin the root carries coverage= (L-W present-only), whose clause rode the confidence sentence. +// routeWasOn route= rode this answer, so its reading was there to lose. +// scWasOn a served row carries sc=, so the sc= reading was there to lose (default dialect only). +inline std::string legendDroppedNote( bool compact, bool thin, bool routeWasOn, bool scWasOn ) { - h += ""; // M10: at= folded into the SAME trailing comment as root= (one wrapper, not two) — the @@ -684,7 +846,7 @@ inline constexpr std::string_view kForAutoBundleLegend = inline constexpr std::string_view kForCompactBundleLegend = "; bundle=compact: conceptual query, so this map ships one-hop EDGE context, no bodies (bodies=0, " "reason=compact-route or no_candidates). hops rows are h l=line p=file n=name, and a row's calls " - "child names its callees (c n= l=). hops and calls disclose total=requested shown=printed capped=1 " + "child names its callees (c n= l=; l= comma-joins the lines of same-named callees, shown= counts callees). hops and calls disclose total=requested shown=printed capped=1 " "when the BUDGET cut a listing; noedge=N counts ranked symbols with no RESOLVED callee found (never " "none exists). For a body: expand=p:n pasted off a row; the auto-bodies flag puts the bodies back"; @@ -706,7 +868,7 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout p.compactLegend ? "ripwire.for/v1" : std::string_view() ); if( p.compactLegend ) { - appendCompactForLegend( h, p, extraNotes ); + appendCompactForLegend( h, p, withRouteAttr, extraNotes ); return h; } h += ""; @@ -989,7 +1164,20 @@ struct ForLensRootFinish bool bodyCeiling = false; }; -inline std::string finishForLensHeader( std::string header, const ForLensRootFinish& f ) +// PR #215 review: the finished header AND the number it prints, handed back together. finishForLensHeader +// below is this function's header half and stays the name every emit site calls; the ceiling ladder's exact- +// ceiling test needs the OTHER half, because "does this document fit what its root promises" is the question +// `est_tokens <= budget_tokens` asks and nothing else answers it. Computing it a second time beside this +// fixpoint is how the two came to disagree on the RATE in the first place, so there is one fixpoint and two +// readers of it. `estTokens` is the number the document will actually carry — the second-stage value when +// over_ceiling="1" rides, since those bytes are part of the document the number prices. +struct ForLensPricedHeader +{ + std::string header; + std::size_t estTokens = 0; // 0 on the unmeasured degrade path (`f.measured` false — no number is printed there) +}; + +inline ForLensPricedHeader finishForLensHeaderPriced( std::string header, const ForLensRootFinish& f ) { // T3: the bundle=auto disclosure attributes, then budget_bytes= (its presence is decided by the sigs render), // then the INDEXING-cap attributes (root facts of the RANKING, so on the root est_tokens prices rather than in @@ -1013,7 +1201,7 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin spliceBefore( header, " -->", /*fromEnd=*/true, f.capNote ); if( !f.measured ) { - return header; + return { std::move( header ), 0 }; } // N1: the legend clause defining est_tokens= goes into the LAST comment first, so header.size() below @@ -1064,7 +1252,12 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin // N1: onto the root — the same ">def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file +def pin_page( self, page_id ):def evict_one( cache ):class PageCache:def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holdsdef flush_cache( self ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelog# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributing# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the… \ No newline at end of file diff --git a/test/docdemotegolden_noroute.xml b/test/docdemotegolden_noroute.xml index 63be64d95..9a699e69c 100644 --- a/test/docdemotegolden_noroute.xml +++ b/test/docdemotegolden_noroute.xml @@ -1,4 +1,4 @@ -# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):# Bug report# Bug report **Describe the bug** A clear and concise description of what the bug is. **Steps to reproduce** Steps to reproduce the behavior: 1. Go to the page and open it 2. Pin a page and flush the cache 3. See the error **Expected behavi…class PageCache:# Contributing Thanks for reporting a bug. Please describe the bug, list the steps to reproduce it, and state the expected behavior before opening a pull request about the page cache, its eviction policy, or a pinned page during a flush.# Contributingdef pin_page( self, page_id ):# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…# Tier probe# Tier probe A pinned page, an eviction, and a flush of the page cache described in exactly the same words twice, so two documents differing only in where they live can be compared directly. Describe the bug, the steps to reproduce, and the…def evict_one( cache ):def flush_cache( self ):def request_page( self, page_id ):# Paging design# Paging design The page cache holds a fixed number of pages. A pinned page is one the reader still holds## Unreleased# Changelog ## Unreleased - Fixed the RuntimeError raised during eviction of a pinned page in the page cache during flush. The traceback pointed at runpy and at the exec of run_globals in _run_module_as_main, so the failure read as a startu…# Changelogdef __init__( self, capacity ):/dev/null )" [ -n "$OUT" ] || { echo "no output — binary or fixture broken"; exit 2; } # ── 1) exactly one row carries id="...Box::data" — the const/non-const pair is collapsed ────────────── -n_rows="$( printf '%s' "$OUT" | grep -o 'id="box.h::Box::data"' | wc -l | tr -d ' ' )" +n_rows="$( printf '%s' "$OUT" | grep -o 'n="data" sc="Box"' | wc -l | tr -d ' ' )" [ "$n_rows" = 1 ] \ && ok "the Box::data overload pair collapses to exactly one row (was 2, byte-identical, pre-fix)" \ || no "expected exactly 1 row for id=\"...Box::data\", got $n_rows" # ── 2) the surviving row discloses the multiplicity via overloads="2" ────────────────────────────────── -printf '%s' "$OUT" | grep -q 'id="box.h::Box::data"[^>]*overloads="2"' \ +printf '%s' "$OUT" | grep -q 'n="data" sc="Box"[^>]*overloads="2"' \ && ok "the collapsed row carries overloads=\"2\" (the id is the same, so 2 rows carried zero extra info)" \ || no "collapsed row is missing overloads=\"2\": $( printf '%s' "$OUT" | grep -o ']*Box::data[^>]*>' )" diff --git a/test/elixircheck.sh b/test/elixircheck.sh index a8e509953..814634a2a 100755 --- a/test/elixircheck.sh +++ b/test/elixircheck.sh @@ -113,9 +113,11 @@ EX "$BIN" "$TMP/boundaries" --no-cache > "$TMP/boundaries.xml" python3 - "$TMP/boundaries.xml" <<'PYBOUND' import sys, xml.etree.ElementTree as ET -rows = list(ET.parse(sys.argv[1]).iter('s')) +tree = ET.parse(sys.argv[1]) +rows = list(tree.iter('s')) syms = {s.get('n').split('/')[0]: s for s in rows} -byid = {s.get('id'): s for s in rows if s.get('id')} +# row 6 (2026-09-12): the row prints sc= (the scope); its canonical id composes as ::sc::n +byid = {f.get('p') + '::' + s.get('sc') + '::' + s.get('n'): s for f in tree.iter('f') for s in f.iter('s') if s.get('sc')} def calls(node): return {c.get('n').split('/')[0] for c in node.iter('c')} # `defimpl P, for: T` defines the module Elixir itself generates, `P.T`, and its clauses are ordinary # executable functions. They are indexed under that scope, so a name the enclosing module also defines diff --git a/test/elixirnamearitycheck.sh b/test/elixirnamearitycheck.sh index ace443658..a8ead0778 100755 --- a/test/elixirnamearitycheck.sh +++ b/test/elixirnamearitycheck.sh @@ -98,9 +98,12 @@ callees(){ python3 - "$1" "$2" <<'PY' import sys, xml.etree.ElementTree as ET root = ET.parse(sys.argv[1]).getroot() -for s in root.iter('s'): - if (s.get('id') or '').endswith(sys.argv[2]): - print(' '.join(sorted(c.get('n') for c in s.iter('c')))) +# row 6: the row prints sc= (the scope) and its the path — compose the canonical id the old id= spelled +for f in root.iter('f'): + for s in f.iter('s'): + cid = f.get('p') + '::' + s.get('sc') + '::' + s.get('n') if s.get('sc') else s.get('n') + if cid.endswith(sys.argv[2]): + print(' '.join(sorted(c.get('n') for c in s.iter('c')))) PY } header_unresolved(){ grep -oE 'unresolved=[0-9]+' "$1" | head -1; } @@ -226,11 +229,11 @@ firstd(){ grep -oE ']*>' "$1" | head -1 | grep -oE ' n="[^"]*"'; } || no "(D) --for=generate_app: first row $( firstd "$TMP/for_snake.xml" ); ctx $( grep -oE 'reason="[^"]*"' "$TMP/for_snake.xml" | head -1 )" # the fixture copy is scanned by an absolute path, so the evidence is the elided form `/.../work.ex` # (lexical.h routeAnchorPath); what matters is that it is a FILE, not the literal `syntax` of a name-less hit -grep -qE 'route="routed: name-exact BM25[^"]*anchors: generate_app\([^)]*work\.ex\)' "$TMP/for_snake.xml" \ +grep -qE 'route="name-exact\(generate_app\)[^"]*anchors: generate_app\([^)]*work\.ex\)' "$TMP/for_snake.xml" \ && ok "(D) the name-exact route's anchor evidence names the defining file (not \`syntax\`)" \ || no "(D) anchor evidence wrong: $( grep -oE 'route="[^"]*"' "$TMP/for_snake.xml" | head -1 )" "$BIN" "$F" --for=text --no-cache >"$TMP/for_word.xml" 2>/dev/null -grep -q 'route="routed: name-exact BM25' "$TMP/for_word.xml" && [ "$( firstd "$TMP/for_word.xml" )" = ' n="text/2"' ] \ +grep -q 'route="name-exact(' "$TMP/for_word.xml" && [ "$( firstd "$TMP/for_word.xml" )" = ' n="text/2"' ] \ && ok "(D) --for=text: the plain word is a whole-name hit through the arity-less spelling — name-exact route, text/2 first" \ || no "(D) --for=text: route $( grep -oE 'route="[^ ]* [^ ]*' "$TMP/for_word.xml" | head -1 ) first $( firstd "$TMP/for_word.xml" )" "$BIN" "$F" --for=text/2 --no-cache >"$TMP/for_arity.xml" 2>/dev/null diff --git a/test/elixirsemanticcheck.sh b/test/elixirsemanticcheck.sh index 65e9af37c..951a718a6 100755 --- a/test/elixirsemanticcheck.sh +++ b/test/elixirsemanticcheck.sh @@ -186,7 +186,8 @@ end return subprocess.check_output([sys.argv[1], str(code), *args], env=env) def parse(data): tree = ET.fromstring(data) - return {s.get('id'): s for s in tree.iter('s') if s.get('id')} + # row 6 (2026-09-12): the row prints sc= (the scope); its canonical id composes as ::sc::n + return {f.get('p') + '::' + s.get('sc') + '::' + s.get('n'): s for f in tree.iter('f') for s in f.iter('s') if s.get('sc')} def targets(rows, name, scope='Client'): key = 'apps/client/main.ex::' + scope + '::' + name assert key in rows, ('missing definition', key, sorted(rows)) diff --git a/test/estchargecheck.sh b/test/estchargecheck.sh index 6ee4ec22d..1dd237ba7 100755 --- a/test/estchargecheck.sh +++ b/test/estchargecheck.sh @@ -501,7 +501,12 @@ for i in range( 4 ): fh.write( "\n".join( lines ) ) PYG a7s_bad=""; a7s_badn=0; a7s_runs=0; a7s_inside_labelled=0 -for spec in "default:1200:1500:" "detail_graph:2880:3080:--detail=20 --with-graph"; do +# RE-ANCHORED 2026-09-13 (PR #215): the default sweep starts at 760, not 1200. --for's rung zero now triggers on the +# EXACT ceiling (verbs_for.h), so a document 1..15% over its budget drops its three explanatory clauses before the +# allowance is consulted; on this corpus the late-label band (over_ceiling="1" INSIDE the allowance — the residual +# after that drop) therefore sits at 780..810 instead of inside 1200..1500, and the control below would otherwise be +# inert. Swept 700..3300 step 10 on the new binary: default hits at 780 790 800 810, none on the --detail=20 arm. +for spec in "default:760:1500:" "detail_graph:2880:3080:--detail=20 --with-graph"; do s_label="${spec%%:*}"; s_rest="${spec#*:}"; s_from="${s_rest%%:*}"; s_rest="${s_rest#*:}"; s_to="${s_rest%%:*}"; s_args="${s_rest#*:}" for (( N = s_from; N <= s_to; N += 10 )); do # shellcheck disable=SC2086 @@ -520,7 +525,7 @@ for spec in "default:1200:1500:" "detail_graph:2880:3080:--detail=20 --with-grap done done [ "$a7s_badn" -eq 0 ] \ - && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 1200..1500, --detail=20 --with-graph 2880..3080, step 10) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung" \ + && ok "#11 A7 sweep: $a7s_runs budgets over a git-less corpus (default 760..1500, --detail=20 --with-graph 2880..3080, step 10) — every document within N x 2.36 x 1.15 at exit 0, or on the ladder's disclosed last rung" \ || no "#11 A7 sweep: $a7s_badn of $a7s_runs budgets deliver past the allowance with no ladder rung fired (first:$a7s_bad) — a byte spliced in after the ladder priced the document" # control: the sweep must cross the band the defect lives in — a root that says over_ceiling="1" while the document # still fits the allowance (est_tokens > N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. @@ -1209,5 +1214,98 @@ if command -v xmllint >/dev/null 2>&1; then done fi +# ── #18 (PR #215 review): RUNG ZERO PRICES THE DOCUMENT IT WOULD EMIT, IN THE ROOT'S OWN MIXED RATE ──── +# +# THE DEFECT. --for's ceiling ladder has a rung ZERO below its own three rungs: the droppable legend trio +# (the confidence reading, the r=/ reading, the sc=/route= reading). It fires when the header does not +# fit "the ceiling the root promises", and it used to spell that ceiling in BYTES — the raw document total +# against budget x 2.50 — while the promise itself, est_tokens <= budget_tokens, is a MIXED rate: markup at +# 2.50 B/tok and the --detail / auto bodies at 3.80 B/tok (serialize.h, finishForLensHeader). Every body byte +# was therefore charged 1.52x what the root charges it. The same test also priced the candidate through a sum +# built from RESERVES and from the auto section whether or not that section was rendered — not the document +# stdout receives. Both errors point one way: a document its own root says fits was judged not to, and three +# definitions the reader has no other source for were spent to buy headroom that was already there. +# +# WHAT IS ASSERTED, and why it needs no magic budget. A document that prices at est_tokens=E fits EVERY budget +# >= E, by the root's own arithmetic — so this arm READS E off a wide run where nothing is dropped and probes +# just above it. No pinned byte count: if the corpus or the legend moves, E moves with it and the probe follows. +# Both guards against an empty pass are asserted rather than assumed — the wide run must carry the clauses (else +# there is no E), and the control below must still DROP them (else the rung is gone, not fixed). +# GIT-LESS and relative, the #11 A7 sweep's discipline: no at=, no churn, a fixed root=, nothing from the live +# repo. --detail=1 is what puts bytes at the BODY rate, which is the half of the defect a bodiless bundle cannot +# see; the fixture's one long body exceeds the tight budget's residual, so the first-entry-whole floor emits a +# truncated ~190 B of it at every budget in the band and the band's width is that floor x (1/2.50 - 1/3.80). +# MEASURED on the c4478402 binary: the band is 1069..1099 — 31 budgets at which the kept document prices at +# est_tokens=1069 with no over_ceiling=, and the pre-fix rung dropped all three clauses and delivered 715. +RZ="$TMP/rungzero" +mkdir -p "$RZ/corpus" +python3 - "$RZ/corpus" <<'PYRZ' +import os, sys +out = sys.argv[ 1 ] +lines = [ "def widgetPingBoxRouter( alpha, beta ):", + ' """Widget ping box router: route every alpha reading onto the beta box."""', + " total = 0" ] +for j in range( 26 ): + lines.append( f" total = total + alpha * {j} - beta * {j} + widgetPingStep{j % 4}( total, {j} )" ) +lines.append( " return total" ) +with open( os.path.join( out, "router.py" ), "w" ) as fh: + fh.write( "\n".join( lines ) + "\n" ) +with open( os.path.join( out, "steps.py" ), "w" ) as fh: + for j in range( 4 ): + fh.write( f'def widgetPingStep{j}( total, step ):\n """Step {j}."""\n return total + step\n\n' ) +PYRZ +rz_run(){ ( cd "$RZ" && "$BIN" corpus --for="widget ping box router" --detail=1 --token-budget="$1" --no-cache ) >"$RZ/o.xml" 2>/dev/null; } +rz_est(){ grep -aoE 'est_tokens="[0-9]+"' "$RZ/o.xml" | head -1 | tr -dc '0-9'; } +rz_note(){ grep -acF '[legend clauses:' "$RZ/o.xml"; } +RZ_WIDE=1200 +rz_run "$RZ_WIDE"; RZ_E="$( rz_est )"; RZ_WIDE_NOTE="$( rz_note )" +# THE CLAUSES THIS ARM COUNTS — all THREE of the droppable trio (CodeRabbit, PR #215). It counted two: the +# confidence reading and the tail reading, but not the route= reading the arm's own paragraph above names. +# A clause that is asserted in neither direction is not pinned, and the consequence is measured: with the +# route= reading removed from the binary (forIdRouteLegendParts returning an empty route part), the wide +# control still read clauses=2/2 and the whole arm reported PASS. Counted in all three runs now — the wide +# control, the probe, and the tight control that must have dropped every one of them — so a clause can only +# disappear by failing the wide run or by surviving the control. +RZ_CLAUSE_ROUTE='route= name-exact(X)|subtoken+body' +RZ_CLAUSES=0 +grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CLAUSES=$(( RZ_CLAUSES + 1 )) +if [ -z "$RZ_E" ] || [ "$RZ_WIDE_NOTE" != "0" ] || [ "$RZ_CLAUSES" != "3" ]; then + no "#18 rung zero: the wide control (--token-budget=$RZ_WIDE) does not carry its legend (est='${RZ_E:-unreadable}' dropped-note=$RZ_WIDE_NOTE clauses=$RZ_CLAUSES/3) — there is no price to probe against; re-anchor the fixture" +else + RZ_PROBE=$(( RZ_E + 5 )) + if [ "$RZ_PROBE" -ge "$RZ_WIDE" ]; then + no "#18 rung zero: the probe budget $RZ_PROBE is not strictly below the wide control $RZ_WIDE — the two runs are the same run and the arm proves nothing; raise RZ_WIDE" + else + rz_run "$RZ_PROBE"; RZ_PE="$( rz_est )"; RZ_PN="$( rz_note )"; RZ_PB="$( bytes_of "$RZ/o.xml" )" + RZ_PC=0 + grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_PC=$(( RZ_PC + 1 )) + RZ_PO=0; grep -aqF 'over_ceiling="1"' "$RZ/o.xml" && RZ_PO=1 + if [ "$RZ_PN" = "0" ] && [ "$RZ_PC" = "3" ] && [ -n "$RZ_PE" ] && [ "$RZ_PE" -le "$RZ_PROBE" ] && [ "$RZ_PO" = "0" ]; then + ok "#18 rung zero at --token-budget=$RZ_PROBE (5 tokens above the $RZ_E this document prices at): all three droppable clauses ride, est_tokens=$RZ_PE <= $RZ_PROBE, no over_ceiling=, $RZ_PB B" + else + no "#18 rung zero at --token-budget=$RZ_PROBE dropped a legend it could afford: dropped-note=$RZ_PN clauses=$RZ_PC/3 est_tokens=${RZ_PE:-unreadable} over_ceiling=$RZ_PO ($RZ_PB B) — the same document prices at $RZ_E at --token-budget=$RZ_WIDE, so it fits every budget >= $RZ_E" + fi + fi + # …and the rung must still FIRE where the kept document genuinely does not fit. Without this, deleting + # rung zero outright would turn the arm above green. + RZ_CTRL=$(( RZ_E - 200 )) + rz_run "$RZ_CTRL"; RZ_CN="$( rz_note )"; RZ_CE="$( rz_est )" + # …and the tight control asserts the ABSENCE of the same three, route= included: a present-in-wide / + # unchecked-in-tight assertion is the one-sided shape that let the missing clause through. + RZ_CC=0 + grep -aqF 'confidence= derives from the ranked head' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF 'tail: file-grain tail' "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + grep -aqF "$RZ_CLAUSE_ROUTE" "$RZ/o.xml" && RZ_CC=$(( RZ_CC + 1 )) + if [ "$RZ_CN" != "0" ] && [ "$RZ_CC" = "0" ]; then + ok "#18 rung zero control at --token-budget=$RZ_CTRL (200 under the $RZ_E the full document prices at): all three clauses dropped and the note says so (est_tokens=$RZ_CE) — the rung still fires when the drop is real" + else + no "#18 rung zero control at --token-budget=$RZ_CTRL: dropped-note=$RZ_CN clauses still riding=$RZ_CC/3 (est_tokens=${RZ_CE:-unreadable}) — rung zero no longer fires at all, so the arm above is green for the wrong reason" + fi +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/expandmodecheck.sh b/test/expandmodecheck.sh index b1971dbd3..4f8cf8ffb 100755 --- a/test/expandmodecheck.sh +++ b/test/expandmodecheck.sh @@ -154,5 +154,104 @@ diff -q "$TMP/big.xml" "$TMP/big2.xml" >/dev/null \ && ok "(det) bundle mode byte-identical twice" \ || no "(det) bundle mode differs across two runs" + +# ── (4) THE PRICE IS THE DOCUMENT, AND ONE FUNCTION PRICES BOTH CANDIDATES (CodeRabbit, PR #215) ────── +# +# THE DEFECT. The two candidates were priced by two hand-built counters. The bundle side was charged its +# `` envelope, its root attributes, the unproven residue, the map and the rendered ; the file +# side was charged `wf.rawBytes + the whole-file legend` — no envelope, no root attributes, no ``, and +# the file's RAW bytes rather than the `` blocks that actually carry them. Measured on a 864 B +# file: reason= said "file 1100B" for a document that came out 1263 B. Two consequences, one cause: +# * the reported number is not the document's, so an agent budgeting the next call is told the wrong price; +# * inside a 163 B band the tool chose — and DISCLOSED — the whole-file form while the bundle it rejected +# was the smaller document. Measured on the pre-fix binary at this fixture's 800 B padding: served +# 1262 B under reason="file 1100B < bundle 1193B". +# 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: the bug is the two counters, not the missing addends. Both candidates +# now describe themselves as an ExpandServeDocument and are priced by priceExpandServeDocument (main.cpp), so +# the comparison is symmetric by construction and a third candidate cannot be added asymmetrically. +# +# WHAT IS PINNED. (4a)/(4b) the identity — in each mode, the number reason= attributes to the mode that WON +# equals the delivered document's byte count, exactly. That is the assertion two hand-built counters cannot +# satisfy, and it holds whatever the corpus does, so it pins no magic byte count. (4c) the CHOICE: across a +# padding sweep that straddles the decision boundary in both directions, the document served is never larger +# than the number reason= attributes to the candidate it rejected. (4c) is what goes red inside the band. +reason_num(){ # $1 = xml file, $2 = the word the number follows ("file" or "bundle") + grep -o "$2 [0-9]*B" "$1" | head -1 | tr -dc '0-9' +} +wf_num="$( reason_num "$TMP/small.xml" file )" +if [ -n "$wf_num" ] && [ "$wf_num" = "$total" ]; then + ok "(4a) whole-file mode: reason=\"file ${wf_num}B\" IS the delivered document ($total B) — one price, one document" +else + no "(4a) whole-file mode prices a document it does not serve: reason=\"file ${wf_num:-unreadable}B\" against $total B delivered — the envelope, the root attributes, the wrapper and are uncharged on this side" +fi +bd_num="$( reason_num "$TMP/big.xml" bundle )" +if [ -n "$bd_num" ] && [ "$bd_num" = "$bundleTotal" ]; then + ok "(4b) bundle mode: reason=\"bundle ${bd_num}B\" IS the delivered document ($bundleTotal B) — the same price the file side is compared against" +else + no "(4b) bundle mode prices a document it does not serve: reason=\"bundle ${bd_num:-unreadable}B\" against $bundleTotal B delivered" +fi + +# (4c) the decision boundary, swept. Padding is a block comment, so it grows the FILE without growing the +# body the bundle would serve — which walks the two candidates past each other. The file name and the fixture +# dir are fixed-length on purpose: root="…" rides in both prices, and a length that moves with a mktemp name +# would move the band with it (the fixture-path-length trap this suite records elsewhere). +mkdir -p "$TMP/narrow" +sweep_n=0; sweep_bad=0; sweep_wf=0; sweep_bun=0 +for pad in 600 700 800 900 1000 1100 1200; do + rm -f "$TMP/narrow/n.c" + { printf 'int narrowProbe( int value )\n{\n return value * 2 + 1;\n}\n/*' + python3 -c "import sys; sys.stdout.write( 'x' * $pad )" + printf '*/\n' + } > "$TMP/narrow/n.c" + ( cd "$TMP" && "$BIN" narrow --expand=narrowProbe --no-cache ) >"$TMP/narrow.xml" 2>/dev/null + got="$( wc -c <"$TMP/narrow.xml" | tr -d ' ' )" + sweep_n=$(( sweep_n + 1 )) + if grep -q 'mode="whole-file"' "$TMP/narrow.xml"; then + sweep_wf=$(( sweep_wf + 1 )) + rejected="$( reason_num "$TMP/narrow.xml" bundle )"; label="rejected bundle" + elif grep -q 'mode="bundle"' "$TMP/narrow.xml"; then + sweep_bun=$(( sweep_bun + 1 )) + rejected="$( reason_num "$TMP/narrow.xml" file )"; label="rejected whole-file" + else + sweep_bad=$(( sweep_bad + 1 )) + printf ' pad=%s: no mode= disclosure at all\n' "$pad" + continue + fi + if [ -z "$rejected" ] || [ "$got" -gt "$rejected" ]; then + sweep_bad=$(( sweep_bad + 1 )) + printf ' pad=%s: served %s B, larger than the %s it priced at %s B — %s\n' \ + "$pad" "$got" "$label" "${rejected:-unreadable}" "$( grep -o 'reason="[^"]*"' "$TMP/narrow.xml" | head -1 )" + fi +done +[ "$sweep_wf" -gt 0 ] && [ "$sweep_bun" -gt 0 ] \ + && ok "(4c) the $sweep_n-point sweep straddles the decision boundary ($sweep_wf whole-file, $sweep_bun bundle) — the arm below is not vacuous" \ + || no "(4c) the sweep chose one mode at every padding ($sweep_wf whole-file, $sweep_bun bundle) — it never crosses the boundary, so it proves nothing; re-anchor the padding band" +[ "$sweep_bad" -eq 0 ] \ + && ok "(4c) at all $sweep_n paddings the served document is no larger than the candidate it rejected — the comparison is symmetric" \ + || no "(4c) $sweep_bad of $sweep_n paddings served a document LARGER than the candidate they rejected (listed above) — the two candidates are priced on different accounting" + +# (4d) the one field (4a)/(4b) cannot exercise: mapBytes. Both fixtures above are EXACT-NAME --expand, which +# defaults its own map to top-k=0 — so no ranked map rides and the map term is 0 on both sides. An AMBIGUOUS +# name (two definitions) keeps the default map, and the bundle then wins carrying it: the same identity must +# hold with the map's own bytes inside the price. +mkdir -p "$TMP/ambig" +for f in a b; do + { printf 'int dupSym( int a ) { return a + 1; }\n/*' + python3 -c "import sys; sys.stdout.write( 'y' * 4000 )" + printf '*/\nint other_%s( void ) { return dupSym( 1 ); }\n' "$f" + } > "$TMP/ambig/$f.c" +done +( cd "$TMP" && "$BIN" ambig --expand=dupSym --no-cache ) >"$TMP/ambig.xml" 2>/dev/null +ambTotal="$( wc -c <"$TMP/ambig.xml" | tr -d ' ' )" +amb_num="$( reason_num "$TMP/ambig.xml" bundle )" +if ! grep -q '/dev/null | wc -c | tr -d ' ' )" if python3 -c 'import tiktoken' >/dev/null 2>&1; then "$BIN" src --expand=buildGraph --top-k=200 --no-cache 2>/dev/null >"$TMP/exp3.xml" TRUTH="$( python3 -c 'import sys,tiktoken; print(len(tiktoken.get_encoding("o200k_base").encode(open(sys.argv[1],encoding="utf-8",errors="replace").read())))' "$TMP/exp3.xml" )" LABEL="real o200k" else - TRUTH=$(( FULL_BYTES * 100 / 285 )) # bytes / 2.85 blended proxy - LABEL="byte/2.85 proxy" + TRUTH=$(( FULL_BYTES * 100 / 340 )) # bytes / 3.40 blended proxy (re-calibrated 2026-09-13, see above) + LABEL="byte/3.40 proxy" fi DIFF=$(( EXP_EST - TRUTH )); [ "$DIFF" -lt 0 ] && DIFF=$(( -DIFF )) LIM=$(( TRUTH * 15 / 100 )) diff --git a/test/expandtopk0check.sh b/test/expandtopk0check.sh index e3cbd6bcd..d1720da56 100755 --- a/test/expandtopk0check.sh +++ b/test/expandtopk0check.sh @@ -145,15 +145,27 @@ diff -q "$TMP/real_default_body.xml" "$TMP/real_tk0_body.xml" >/dev/null \ && ok "(G-a) default's served body is byte-identical to explicit --top-k=0's" \ || no "(G-a) default's served body diverges from explicit --top-k=0's — the estimator or the emitter disagree on what mapTopK==0 means" -# (G-b) when topk_default="0" is in effect and a reason= fires, the bundle byte count it PRICES must be -# the REAL served size (== the --top-k=0 byte count), never a phantom map-inclusive estimate. This -# is the precise, load-bearing number the V1 defect corrupted. +# (G-b) when topk_default="0" is in effect and a reason= fires, the bundle byte count it PRICES must be the +# REAL served size, never a phantom map-inclusive estimate. This is the precise, load-bearing number +# the V1 defect corrupted. +# +# WHAT THE COMPARISON IS, and why it moved (CodeRabbit, PR #215). It used to read the priced number +# against EXPLICIT --top-k=0's byte count. Those two documents carry the same payload but not the same +# root: the auto form is decorated with topk_default= and the mode=/reason= disclosure, the explicit +# override is deliberately undecorated ((G-a) strips the opener for exactly that reason). The old +# equality held only because the price omitted that decoration too — two omissions cancelling, which is +# how the file candidate came to be compared against the bundle on different accounting in the first +# place. Both candidates are now priced as the complete document they would serve, so the identity is +# the stronger and more direct one: the priced bundle IS the bundle document. The anti-phantom property +# the arm exists for is unweakened — (G-a) already proves this document's payload is byte-identical to +# explicit --top-k=0's, and a phantom whole-repo map would blow the identity here by ~1 MB. pricedBundle=$( grep -oE 'reason="bundle [0-9]+B' "$TMP/real_default.xml" | grep -oE '[0-9]+' ) +realDefaultBytes=$( wc -c < "$TMP/real_default.xml" | tr -d ' ' ) if [ -n "$pricedBundle" ]; then - if [ "$pricedBundle" = "$realTk0Bytes" ]; then - ok "(G-b) reason= prices the bundle at exactly the --top-k=0 byte count (${pricedBundle}B) — no phantom map" + if [ "$pricedBundle" = "$realDefaultBytes" ]; then + ok "(G-b) reason= prices the bundle at exactly the bytes it serves (${pricedBundle}B; explicit --top-k=0 serves the same payload under an undecorated root, ${realTk0Bytes}B) — no phantom map" else - no "(G-b) reason= priced the bundle at ${pricedBundle}B but --top-k=0 actually serves ${realTk0Bytes}B — the reason= string still prices a map that mapTopK==0 will never emit" + no "(G-b) reason= priced the bundle at ${pricedBundle}B but the document it served is ${realDefaultBytes}B — the whole-file candidate is being compared against a bundle price that is not the bundle document" fi else no "(G-b) no reason=\"bundle NNNB ...\" clause found on the real-repo default root — unexpected mode, see (G) above: $( grep -oE ']*>' "$TMP/real_default.xml" )" diff --git a/test/extentcheck.sh b/test/extentcheck.sh index 9699623ca..6e11ccd5d 100644 --- a/test/extentcheck.sh +++ b/test/extentcheck.sh @@ -106,7 +106,7 @@ cap "$TMP/a.xml" "$TMP/leak" --no-cache SWALLOWED="accumulateOrders clampQuantity hasLabel drainQueue" leaked=0 for n in $SWALLOWED; do - row "$TMP/a.xml" "$n" | grep -q "id=\"leak.cpp::IndiaError::$n\"" && leaked=$(( leaked + 1 )) + row "$TMP/a.xml" "$n" | grep -q "n=\"$n\" sc=\"IndiaError\"" && leaked=$(( leaked + 1 )) # row 6: the scope rides sc= done [ "$leaked" -eq 4 ] && ok "(A) the fixture still reproduces the leak: 4/4 free functions filed under IndiaError::" \ || no "(A) the fixture no longer reproduces the leak ($leaked/4 under IndiaError::) — every other arm is testing nothing" diff --git a/test/fieldnarrowcheck.sh b/test/fieldnarrowcheck.sh index 99e4c7241..05e575449 100755 --- a/test/fieldnarrowcheck.sh +++ b/test/fieldnarrowcheck.sh @@ -112,8 +112,9 @@ MAP="$( "$BIN" "$FIX" --no-cache 2>/dev/null | tr '>' '\n' )" callees(){ "$BIN" "$FIX" "--callees=$1" --no-cache 2>/dev/null | grep -o '' | tr '/' '\n'; } # ── presence guards (a gate that cannot observe what it asserts is green-while-inert) ── -# TS symbols carry no scope string (no scoped id= attribute), so to_go is matched by its n= name. -for want in '::Pool::acquire"' '::Decoy::acquire"' '::Owner::run"' '::Base::helper"' '::DecoyH::helper"' '::Owner4::inh_go"' '::POwner::po_go"' 'n="to_go"'; do +# TS symbols carry no scope string (no sc= attribute), so to_go is matched by its n= name. Row 6: a scoped row +# prints n= then sc= (the short id; the canonical id composes as p::sc::n with the enclosing ). +for want in 'n="acquire" sc="Pool"' 'n="acquire" sc="Decoy"' 'n="run" sc="Owner"' 'n="helper" sc="Base"' 'n="helper" sc="DecoyH"' 'n="inh_go" sc="Owner4"' 'n="po_go" sc="POwner"' 'n="to_go"'; do printf '%s\n' "$MAP" | grep -qF "$want" || no "presence guard: fixture symbol $want not indexed" done [ "$fail" = 0 ] && ok "presence: all fixture symbols indexed" @@ -202,7 +203,7 @@ AMB="$( printf '%s\n' "$MAP" | grep -o 'ambiguous=[0-9]*' | head -1 )" # ── (n) same-NAMED class collision (FIX2): conflicting same-named fields tombstone — NEITHER Dup::go narrows ── MAP2="$( "$BIN" "$FIX2" --no-cache 2>/dev/null | tr '>' '\n' )" -printf '%s\n' "$MAP2" | grep -qF '::Dup::go"' || no "(n) presence guard: Dup::go not indexed in FIX2" +printf '%s\n' "$MAP2" | grep -qF 'n="go" sc="Dup"' || no "(n) presence guard: Dup::go not indexed in FIX2" AMB2="$( printf '%s\n' "$MAP2" | grep -o 'ambiguous=[0-9]*' | head -1 )" [ "$AMB2" = "ambiguous=2" ] \ && ok "(n) both n1::Dup::go and n2::Dup::go stay ambiguous (conflicting field types tombstoned)" \ diff --git a/test/fillordercheck.sh b/test/fillordercheck.sh index 47fb71611..b479cf2dd 100755 --- a/test/fillordercheck.sh +++ b/test/fillordercheck.sh @@ -112,7 +112,17 @@ est_of(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE 'est_tokens=[0-9]+' | # pin moved -- no contract did. EFIX="$( est_of test/fixture )" OFIX="$( order_of test/fixture )" -{ [ "$EFIX" = "884" ] && [ "$OFIX" = "important-first" ]; } \ +# RE-PIN 2026-09-12 (row 6, sc=): 884 -> 894. The map legend's sc= reading (the composition rule id = p::sc::n, +# replacing the shorter id=canonical(...) clause) is +40 B on a fixture whose 14 rows carry ONE scoped symbol, so the +# legend outgrows the row saving here; on a real tree the rows win (this repo's flagless map: -15.3%). Only the pin moved. +# RE-PIN 2026-09-13 (PR #215 review, CodeRabbit 5191303552): 894 -> 895, and NOT because anything grew. The byte MODEL +# still charged each scoped symbol `path::scope::name` -- the id= the row stopped printing -- so it billed the path and +# the name a second time on every scoped row. Charged at what the row prints (` sc=""` + the scope) it prices fewer +# bytes, and the reported est_tokens is emitted-bytes / the MODEL'S OWN rate (tokensForEmittedBytes with +# mapEst.bytesPerToken()), so a model that prices a different mix publishes a different rate: here 2.496 -> 2.497 B/tok +# against an emitted size that did not move one byte. Hence +1, on a document byte-identical to its previous self -- +# this fixture's golden is 2,234 B before and after, and est_tokens= is the ONLY character that differs in it. +{ [ "$EFIX" = "895" ] && [ "$OFIX" = "important-first" ]; } \ && ok "test/fixture (est_tokens=$EFIX) does NOT auto-flip — order=$OFIX (golden neutral)" \ || no "test/fixture unexpectedly changed order or est_tokens (est=$EFIX order=$OFIX)" diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index 09dafdd39..8bf5d8370 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -149,6 +149,8 @@ TABLE = { # ── src/lanes.h — THE REFERENCE SAFE SHAPE ─────────────────────────────────────────────────────────── ( "src/lanes.h", "buf" ): ( 10, "safe", "buf[640] x3: snprintf-THEN-escape. :723 interpolates an UNBOUNDED file path and is still safe for exactly that reason — the warning text is escaped downstream, so a cut shortens prose and can never land inside markup. This is the shape §B14's six were not." ), # ── src/main.cpp ───────────────────────────────────────────────────────────────────────────────────── + ( "src/main.cpp", "fileOpen" ): ( 1, "safe", "fileOpen[200] in chooseExpandServe (PR #215 review): '' — 53 B of literal and TWO std::size_t byte counts, 20 digits each at absolute most, so 93 B against 199 usable + NUL: 106 B of margin. No string interpolation at all, so no escaper can sit on either side of the buffer; the < is written as an entity IN THE LITERAL, not produced by escapeXml. The bound matters twice here, because the caller reads std::strlen of this buffer back as the disclosure's own byte length — a truncated write would make the price it charges wrong as well as the document malformed, which is why the row states the margin rather than just the class. It is a TABLE row and not NUMERIC_ONLY for arch.h hex[17]'s reason: a buffer that never existed before the std::print conversion has no pre-conversion format to derive a class from." ), + ( "src/main.cpp", "bundleOpen" ): ( 1, "safe", "bundleOpen[200] in chooseExpandServe (PR #215 review): '' — fileOpen's exact twin, the same two std::size_t and no string: 50 B of literal + 40 digits = 90 B against 199 usable + NUL, 109 B of margin. Same strlen-read-back, same reason for being a row rather than a derivation." ), ( "src/main.cpp", "tail" ): ( 1, "not-markup", "tail[48]: the shallow-clone cache DIR suffix (\"/ripwire-remote-\" + a fixed-width 16-hex). Bounded and never emitted. Was 2 sites: defaultCachePath's cache FILENAME left this buffer when the root-key unification moved its assembly into quality.h::rootKeyedCachePath, which is where its row now lives." ), ( "src/verbs_for.h", "nb" ): ( 14, "safe", "nb[160] x2: the mention/doc-mention/siblift/expand header notes. Every %s is the plural '' or 's'; everything else is %u." ), ( "src/verbs_report.h", "exemptAttr" ): ( 1, "safe", "exemptAttr[40]: ' exempt=\"%s\"' with groupExemptKind's fixed vocabulary (longest 'fixture' = 7 B, total 19 B)." ), @@ -218,7 +220,7 @@ NUMERIC_ONLY = { ( "src/ingest_docpass.h", "blobName" ): 1, ( "src/main.cpp", "hdr" ): 1, ( "src/main.cpp", "nb" ): 4, - ( "src/main.cpp", "open" ): 4, + ( "src/main.cpp", "open" ): 2, # PR #215 review: chooseExpandServe's four openers became two early-return refusals plus the two rowed buffers above ( "src/mcp.h", "buf" ): 1, ( "src/mcpedit.h", "name" ): 1, ( "src/mcpedit.h", "oldStamp" ): 1, @@ -248,9 +250,14 @@ NUMERIC_ONLY = { ( "src/serialize.h", "gfb" ): 1, ( "src/serialize.h", "inAttr" ): 1, ( "src/serialize.h", "kbuf" ): 1, - ( "src/serialize.h", "lb" ): 2, + ( "src/serialize.h", "lb" ): 4, # row 6 (2026-09-12): collectCalleeNameRow's line buffer (the merged row) joined the two + # …and a FOURTH (2026-09-13, PR #215 item 8): appendMergedCalleeNameRows joins the row's + # line numbers itself now, because l= is sorted ASCENDING at append time rather than + # accumulated as text in walk order. Same shape as the site three lines above it — + # "{}" of one std::uint32_t, ten digits worst case against 15 usable + NUL, no %s and + # nothing escaped, so it does not join the string-interpolating population ( "src/serialize.h", "lineAttr" ): 1, - ( "src/serialize.h", "nb" ): 2, + ( "src/serialize.h", "nb" ): 1, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, ( "src/serialize.h", "rc" ): 2, @@ -431,6 +438,27 @@ if not bad: # format to derive a class from — the same reason nestAttr and escAttr are rows. mentions is +2 because # the comment on that buffer names formatTo as well; arch.h's third mention, its emit.h include line, # predates this change. The code it replaced wrote through emitRaw/emitTo, which this gate does not count. +# 2026-09-13 (PR #215 review item 8, the merged row's ascending l=): +1 call/+1 mention, +# +1 site, rows/widthforms UNMOVED — re-derived from `git diff 6e8dd75a -- src/`, not accepted from +# the delta. The one new call is serialize.h appendMergedCalleeNameRows' `char lb[16]`, which joins +# the EXISTING ( serialize.h, lb ) row above (3 -> 4 sites) rather than opening a new one: the row +# now holds four buffers of that name in that file. MergedCalleeNameRow stopped carrying its lines +# as accumulated text — the walk pushes uint32 line numbers and the append sorts them ascending, so +# one fact stopped having two spellings between queries — and the digits are formatted here instead. +# "{}" of one std::uint32_t: no %s, nothing escaped, ten digits worst case against 15 usable + NUL, +# so it does not join the string-interpolating population and rows is unmoved. +# 2026-09-14 (PR #215 review, --expand serving priced by ONE function): mentions 323 -> 324, rows +# 92 -> 94, calls/sites/widthforms UNMOVED at 219/219/0 — re-derived by reading every site, not +# accepted from the delta. chooseExpandServe used to write its four `` openers +# into one `open[160]`; the comparison is now made by a fixpoint that must charge each candidate +# the spelling IT would carry, so the two COMPARED openers moved into their own buffers +# (`fileOpen[200]`, `bundleOpen[200]`, both NEW TABLE rows above) and `open[200]` kept only the two +# refusal paths that make no comparison — 4 sites - 2 + 1 + 1 = 219, which is why the call and site +# totals do not move while rows gains exactly the two new buffers. Every one of the four formats +# interpolates std::size_t byte counts and nothing else: no %s, no path, no name, nothing that has +# been through escapeXml, so none of them is the escape-then-buffer shape this gate exists to catch. +# mentions is +1 and not +2 because the two new buffers are written by two formatTo calls on lines +# that already carried one between them. # 2026-09-14 (review of #214, --pr-context's est-unmeasured disclosure): mentions 323 -> 324, # calls/sites/rows/widthforms ALL UNCHANGED at 219/219/92/0 — re-read from `git diff` and not # accepted from the delta. The single added line is a COMMENT, not a call: prBudgetTail's buffer @@ -439,7 +467,19 @@ if not bad: # because formatTo's SILENT truncation is precisely what was not saving the old bound — it writes # at most cap-1 and its return is not read there, so an overrun would have dropped the closing # quote of truncated=" and shipped a malformed root. Same buffer, same one call, same one row. -EXPECTED = { "mentions": 324, "calls": 219, "sites": 219, "rows": 92, "widthforms": 0 } +# 2026-09-14 (MERGE of lane/sc-legend with main at 0b118ac1 — NEITHER SIDE'S NUMBER WAS RIGHT): +# mentions 326, calls 220, sites 220, rows 94. Both sides of this line said 324, and both were +# correct about their own tree: this lane re-derived 323 -> 324 for the chooseExpandServe split, +# and #214 re-derived 323 -> 324 for the prBudgetTail comment. They are not the same +1, the two +# lanes did not share a base (#214's diff shows 322/218/218/92 as ITS base), and main also carried +# packtask.h's `open` row from 2 sites to 3 — so the merged total is neither 324 nor the 325 that +# adding the two deltas would give. It was DERIVED by running the gate on the merged tree, which is +# the only method this file accepts, and the derivation is corroborated rather than assumed: (S1) +# classifies all 220 sites and (S2) reports no stale or miscounted row, so every member is +# accounted for and every row's multiplicity already matches source — the two new TABLE rows of +# this lane (fileOpen[200], bundleOpen[200]), `main.cpp open` at 2, and packtask.h's `open` at 3 +# among them. Only the totals line moved. +EXPECTED = { "mentions": 326, "calls": 220, "sites": 220, "rows": 94, "widthforms": 0 } # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the # H9 ` max_tokens="%d"` ceiling disclosure: a single %d of a caller-supplied INTEGER, no %s, diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 79a7c5735..bf0a26f62 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -128,7 +128,15 @@ REPO_Q=( "rank graph teleport" "compact legend rewrite" "edit receipt post-check # 10463 10337 10409 10431 9799 9726 10337 10399 10294 10468, i.e. +324..+329 B on every query = the coverage= root fact, # its legend clause (kForCoverageLegend, forpage.h) and the r=1 row's widening next= on a thin answer. Nothing else moved # (routecheck/anchorcheck's goldens re-pinned the same day with every other byte proven identical). -REPO_BASE=( 10134 10029 10080 10102 9470 9397 10009 10070 9966 9861 ) +# q5 RE-PINNED 2026-09-13 (merge of lane/sc-legend and lane/for-widen), the q5 precedent above, third time on the same +# query: 9,470 -> 9,880 B (+4.33%). ATTRIBUTED FOUR WAYS, which is what the precedent requires — main tree / main binary +# 9,464 B, LANE tree / main binary 9,470 B (so corpus drift is +6 B, not the cause), main tree / lane binary 9,875 B, +# lane tree / lane binary 9,880 B: the whole +410 B is the TOOL, and it is +410 B of ANSWER. Main serves this query +# shown="20" of 40 ranked rows; this build serves shown="25". The sc= rows are ~20 B shorter than the id= rows they +# replace, so the byte-shaped section fits five more signature rows, and the legend that grew 154 B (the sc= +# rule, the route= code's reading and the merged-callee reading) is paid for several times over in rows. The other +# nine stay at their for-widen bases: -1.11%, +0.36%, -0.45%, +1.54%, +0.02%, +1.66%, +0.18%, -0.56%, +2.30%. +REPO_BASE=( 10134 10029 10080 10102 9880 9397 10009 10070 9966 9861 ) # ── (1)+(2) rank order + p= on every row, four dialects ─────────────────────────────────────────────────── order_fail=0 diff --git a/test/golden.xml b/test/golden.xml index 3185db791..f10af2737 100644 --- a/test/golden.xml +++ b/test/golden.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/kotlincheck.sh b/test/kotlincheck.sh index 06d9449eb..567e6114e 100755 --- a/test/kotlincheck.sh +++ b/test/kotlincheck.sh @@ -155,18 +155,18 @@ if grep -q ' edges=14 ' "$MAP_OUT"; then ok "header: edges=14"; else no "header: if grep -q ' ambiguous=0 ' "$MAP_OUT"; then ok "header: ambiguous=0"; else no "header: expected ambiguous=0: $( grep -o 'ambiguous=[0-9]*' "$MAP_OUT" )"; fi if grep -q 'unresolved=0' "$MAP_OUT"; then ok "header: unresolved=0"; else no "header: expected unresolved=0: $( grep -o 'unresolved=[0-9]*' "$MAP_OUT" )"; fi -grep -q 'id="Greeter.kt::Greeter::of"' "$MAP_OUT" && ok 'scope: companion-object factory carries id=Greeter.kt::Greeter::of' \ +grep -q 'n="of" sc="Greeter"' "$MAP_OUT" && ok 'scope: companion-object factory carries sc=Greeter (id Greeter.kt::Greeter::of)' \ || no "scope: Greeter::of id missing — kotlinEnclosingScopeOf regressed: $( grep -o 'n="of"[^>]*' "$MAP_OUT" )" -grep -q 'id="Greeter.kt::Greeter::greet"' "$MAP_OUT" && ok 'scope: member function carries id=Greeter.kt::Greeter::greet' \ +grep -q 'n="greet" sc="Greeter"' "$MAP_OUT" && ok 'scope: member function carries sc=Greeter (id Greeter.kt::Greeter::greet)' \ || no "scope: Greeter::greet id missing: $( grep -o 'n="greet"[^>]*' "$MAP_OUT" )" -grep -q 'id="Util.kt::Formatter::format"' "$MAP_OUT" && ok 'scope: plain class member carries id=Util.kt::Formatter::format' \ +grep -q 'n="format" sc="Formatter"' "$MAP_OUT" && ok 'scope: plain class member carries sc=Formatter (id Util.kt::Formatter::format)' \ || no "scope: Formatter::format id missing: $( grep -o 'n="format"[^>]*' "$MAP_OUT" )" -grep -q 'id="Util.kt::Extra::helper"' "$MAP_OUT" && ok 'scope: object member carries id=Util.kt::Extra::helper' \ +grep -q 'n="helper" sc="Extra"' "$MAP_OUT" && ok 'scope: object member carries sc=Extra (id Util.kt::Extra::helper)' \ || no "scope: Extra::helper id missing: $( grep -o 'n="helper"[^>]*' "$MAP_OUT" )" # Negative: a TOP-LEVEL function must NOT pick up a spurious scope (kotlinEnclosingScopeOf's own # self-exclusion / walk-through-anonymous-companion logic must not over-fire). -echo "$( grep -o ']*>' "$MAP_OUT" )" | grep -q 'id=' \ - && no "scope: top-level extension function doubled() got a spurious id= (should be scope-less)" \ +echo "$( grep -o ']*>' "$MAP_OUT" )" | grep -q 'sc=' \ + && no "scope: top-level extension function doubled() got a spurious sc= (should be scope-less)" \ || ok "scope: top-level extension function doubled() is correctly scope-less" CR="$( "$BIN" "$FIX" --callers=square --no-cache 2>/dev/null )" diff --git a/test/legendcoverage_baseline.txt b/test/legendcoverage_baseline.txt index 1444cf319..966da1731 100644 --- a/test/legendcoverage_baseline.txt +++ b/test/legendcoverage_baseline.txt @@ -152,13 +152,11 @@ exercises | exercises@test_symbols external-surface | external-surface@capped external-surface | external-surface@shown external-surface | x@lang -for | ctx@route for | ctx@task for | field@name for | field@owner for | field@rel for | field@type -for-auto | ctx@route for-auto | ctx@task grep | grep@pattern hotspots | f@top_ccx @@ -256,7 +254,6 @@ for | d@pure # the gate's own arm (F) census, which prints both elements' keys on every run and reds when a new one appears. for-budgeted | ctx@bodies for-budgeted | ctx@reason -for-budgeted | ctx@route for-budgeted | ctx@task for-budgeted | d@next for-budgeted | d@pure diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index 30a019e7f..418418952 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -370,8 +370,13 @@ CORE = { "p", "n", "t", "id", "l", "k", "c" } # elements' trios are here because `defined` sees one name: 's three are what the note is ABOUT, 's # three are legendOff's and are closed by the same three words. Do not add a line here to make a red go away # without saying, in the commit, which clause was dropped and why it is not worth a sentence of its own. +# SHRUNK 2026-09-13 (PR #215 review item 10): the compact dialect's four lines stopped reproducing and the arm +# had been printing "verify, then shrink the FLOOR in this file" for four runs without anyone shrinking it — a +# floor that outlives its findings is a floor nobody reads. They are gone because this dialect's rung zero no +# longer fires on these documents: it is taken only when the drop PAYS (verbs_for.h), and in the compact dialect +# the note was longer than the clauses it replaced, so the readings ride and close their own attributes. FLOOR = { "default": { "sigs@capped", "sigs@shown", "sigs@total", "tail@capped", "tail@shown", "tail@total" }, - "compact": { "sigs@shown", "sigs@total", "tail@shown", "tail@total" } } + "compact": set() } LEAD = re.compile( rb'\A(?:\s*)+', re.S ) ATTR = re.compile( rb'<([a-zA-Z][\w-]*)((?:\s+[\w:.-]+="[^"]*")*)\s*/?>' ) diff --git a/test/lpincheck.sh b/test/lpincheck.sh index cb86a39b6..ca4a2e3c8 100755 --- a/test/lpincheck.sh +++ b/test/lpincheck.sh @@ -39,7 +39,8 @@ echo "lpincheck: BIN=$BIN CORPUS=$CORPUS" "$BIN" "$CORPUS" --pin-census="$TMP/c.tsv" --no-cache >"$TMP/map.xml" 2>"$TMP/err" || { no "the map run exited non-zero"; sed 's/^/ /' "$TMP/err"; } MAP="$( cat "$TMP/map.xml" )" -row(){ printf '%s' "$MAP" | tr '<' '\n' | grep "id=\"$1\"" | head -1; } +# row 6 (2026-09-12): a scoped row prints n= then sc= (the short id); the canonical id composes as ::sc::n +row(){ _n="${1##*::}"; _r="${1#*::}"; _s="${_r%::*}"; printf '%s' "$MAP" | tr '<' '\n' | grep "n=\"$_n\" sc=\"$_s\"" | head -1; } # ── (A) the pin is DISCLOSED on its row, and it is still not an amb ────────────────────────────── RUN_ROW="$( row 'pinned.py::Alpha::run' )" @@ -47,7 +48,7 @@ printf '%s' "$RUN_ROW" | grep -q 'lpin="1"' && ok "(A) pinned.py::Alpha::run car || no "(A) pinned.py::Alpha::run has no lpin=\"1\": $RUN_ROW" printf '%s' "$RUN_ROW" | grep -q 'amb=' && no "(A) pinned.py::Alpha::run carries amb= — the marker inflated amb=: $RUN_ROW" \ || ok "(A) the pin still contributes nothing to amb=" -N_HELPER="$( printf '%s' "$MAP" | tr '>' '\n' | awk '/id="pinned.py::Alpha::run"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="helper"' )" +N_HELPER="$( printf '%s' "$MAP" | tr '>' '\n' | awk '/n="run" sc="Alpha"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="helper"' )" if [ "$N_HELPER" = 1 ]; then ok "(A) the pin still emits ONE confident edge"; else no "(A) $N_HELPER helper edges on Alpha::run, want 1"; fi # ── (B) the tied control — a split is not a pin ─────────────────────────────────────────────────── @@ -129,7 +130,7 @@ class Subscriber: return 2 PYEOF "$BIN" "$SELFD" --no-cache >"$TMP/selfwin.xml" 2>/dev/null -SW="$( sed 's/>\n/dev/null | python3 "$TMP/mcptext.py"; } -NR_Q="parse tree" +# RE-AUTHORED 2026-09-12 (row 6, route= as a code): "parse tree" routes subtoken+body:broad on this corpus — the SAME +# ranker no_route forces — so the served sets could only ever differ by what the 97 B route note displaced from the +# byte-shaped ; with the note down to 23 B they were identical, and the arm went red on an artefact. An +# identifier query routes name-exact, so no_route:true really changes the ranker (and the served set) here. +NR_Q="escapeXml" mcp_for "$NR_Q" >"$TMP/nr.routed.xml" mcp_for_nr "$NR_Q" true >"$TMP/nr.off.xml" cli_for "$NR_Q" >"$TMP/nr.cli.routed.xml" @@ -290,5 +294,50 @@ else printf ' SKIP xmllint (not installed)\n' fi + +# ── (7) THE sc= READING IS PRESENT-ONLY ON BOTH DIALECTS (CodeRabbit, PR #215) ─────────────────────────── +# THE DEFECT. The CLI lens made both droppable readings present-only — the sc= clause rides only when a row +# this bundle could serve actually carries a scope (verbs_for.h forScPresent), the route= code only when the +# root carries route= — while this twin appended kForIdRouteLegend UNCONDITIONALLY. On a scope-free corpus the +# MCP answer therefore DEFINED an attribute no row carried, and the signatures-budget exemption a few lines +# below hand-built the same decision a second time, so the bytes emitted and the bytes exempted could disagree. +# Both surfaces now ask rw::forIdRouteLegendParts once and use `.sc`/`.route` to append and `.bytes()` to +# exempt: four sites, one rule. +# +# WHY BOTH DIRECTIONS. A one-sided pin (absent on a flat corpus) would pass on a binary that never emits the +# clause at all, which is the opposite defect — a first-screen attribute with no definition anywhere in the +# document. So the arm reads the same rule twice: the scope-free fixture must have it on NEITHER dialect, and +# the scoped corpus must have it on BOTH. RED on the pre-fix binary: flat corpus, CLI 0, MCP 1. +SC_CLAUSE='sc=scope (full id p::sc::n)' +FLAT="$TMP/flatcorpus" +mkdir -p "$FLAT" +cat > "$FLAT/flat.c" <<'FLATSRC' +int widgetPingRouteAlpha( int a ) { return a + 1; } +int widgetPingRouteBeta( int a ) { return widgetPingRouteAlpha( a ) + 2; } +int widgetPingRouteGamma( int a ) { return widgetPingRouteBeta( a ) + 3; } +FLATSRC +mcp_for_at(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s"}}}\n' \ + "$1" "$2" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +FLAT_Q="widget ping route" +"$BIN" "$FLAT" --for="$FLAT_Q" --no-cache >"$TMP/sc_flat_cli.xml" 2>/dev/null +mcp_for_at "$FLAT" "$FLAT_Q" >"$TMP/sc_flat_mcp.xml" +cli_for "$INERT_Q" >"$TMP/sc_scoped_cli.xml" +mcp_for "$INERT_Q" >"$TMP/sc_scoped_mcp.xml" +sc_has(){ grep -qF "$SC_CLAUSE" "$1" && echo 1 || echo 0; } +# the premise: both fixtures must have produced a bundle with a header at all +if ! grep -q ' bundle — the sc= presence arm measured nothing" +else + flatC="$( sc_has "$TMP/sc_flat_cli.xml" )"; flatM="$( sc_has "$TMP/sc_flat_mcp.xml" )" + scopC="$( sc_has "$TMP/sc_scoped_cli.xml" )"; scopM="$( sc_has "$TMP/sc_scoped_mcp.xml" )" + { [ "$flatC" = 0 ] && [ "$flatM" = 0 ]; } \ + && ok "(7) scope-free corpus: NEITHER dialect defines sc= (CLI=$flatC MCP=$flatM) — no reading for an attribute no row carries" \ + || no "(7) scope-free corpus: the sc= reading is present-only on one dialect and unconditional on the other (CLI=$flatC MCP=$flatM) — the parity gap" + { [ "$scopC" = 1 ] && [ "$scopM" = 1 ]; } \ + && ok "(7) scoped corpus ($CORPUS): BOTH dialects define sc= (CLI=$scopC MCP=$scopM) — the attribute a reader meets has a definition" \ + || no "(7) scoped corpus ($CORPUS): sc= rides the rows but only $((scopC + scopM)) of 2 dialects define it (CLI=$scopC MCP=$scopM)" +fi + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 248141f43..09abe4b36 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -209,6 +209,16 @@ tools = json.loads( line )[ "result" ][ "tools" ] # this twin. Its description gained NO prose: a first draft named coverage= and the page there (+109 B) and was # removed rather than re-anchored around, the L7 precedent above — the schema properties are where a client renders # an argument, and the answer's own legend defines coverage= and the page. Headroom after this line: 23 B. +# RE-ANCHORED 2026-09-13 (PR #215, owner question "how does an agent ask for the longer answer"): 42,200 -> 42,700, +# measured 42,636 B. The `legend` field's description gains one clause — "full (restores the full legend)", +# +32 B on each of the 17 tools that declare the field (+544 B) — so the schema an agent reads when choosing the +# argument's value says what "full" does. This is PROSE on a declared argument, which rule 5 above does not move +# the ceiling for; the owner asked for exactly this clause and authorized moving the pin with the measured number +# (2026-09-13 06:30), and that authority and this attribution are the whole justification. Headroom after: 64 B. +# MERGED 2026-09-13 (lane/sc-legend + lane/for-widen): both anchors above are real and they add. 42,200 -> 42,900, +# RE-MEASURED on the merged tree at 42,820 B (descriptions 19,967, schemas 18,426, 31 tools), not summed from the two +# lanes' separate anchors (42,384 and 42,700), because neither lane could see the other's bytes. Headroom after this +# line: 80 B, less than one declared argument, which is rule 5 above working. # RE-ANCHORED 2026-09-13 (review of #214): 42,384 → 42,800 = +416 B, EXACTLY the one 207-byte clause plus the # single space that separates it from the sentence before it (208 B), spliced into EACH of the TWO tool # descriptions that serve tests_to_run rows as JSON — `situational_awareness` and `explore`; 2 × 208 = 416 @@ -228,7 +238,17 @@ tools = json.loads( line )[ "result" ][ "tools" ] # still said once. The quotes around the two keys are SINGLE: this string is spliced straight into the # tools/list JSON, and the first draft's double quotes made the manifest unparseable — which this gate # caught as a JSONDecodeError, not as a byte count. Measured 42,973. Headroom after this line: 27 B. -CEILING = 43000 +# MERGED 2026-09-14 (lane/sc-legend + main at 0b118ac1): 43,000 -> 43,500, RE-MEASURED on the merged tree at +# 43,432 B (descriptions 20,579, schemas 18,426, 31 tools). NEITHER side's ceiling holds and neither is wrong: +# this lane re-anchored to 42,900 from a measurement of 42,820, #214 re-anchored to 43,000 from 42,973, and +# the two clauses are DIFFERENT bytes in different stanzas, so they add. Not summed from the two anchors — +# the lanes did not share a base and neither could see the other's bytes — and not derived from the deltas +# either; measured on the tree that ships, the 2026-09-10 two-lane precedent above verbatim. No NEW allowance +# is taken here: both adds were justified where they landed (this lane's is prose on a declared argument the +# owner authorized on 2026-09-13 with the measured number; #214's names the response shape for two answers +# that carry no legend at all), and the merge only makes them visible together. Headroom after this line: +# 68 B, again less than one declared argument, which is rule 5 above working. +CEILING = 43500 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) diff --git a/test/mdsectioncheck.sh b/test/mdsectioncheck.sh index 71fb35e3d..907a1fe6a 100755 --- a/test/mdsectioncheck.sh +++ b/test/mdsectioncheck.sh @@ -219,13 +219,15 @@ grep -q $'n="CRLF Heading\r' "$MAP" && no "a CR byte survived into a CRLF headin || ok "CRLF heading name carries no CR byte" echo "--- hierarchy: a section's scope is its parent heading (canonical id path::Parent::Child) ---" -grep -qF '::Orientation Guide::Cache Warm Path"' "$MAP" \ +# row 6 (2026-09-12): the row prints n= then sc= (the scope CHAIN, e.g. sc="Deployment Rollout Setext::Rollback Plan"); +# the canonical id composes as ::sc::n, so "scoped under X" is "sc= ends with X" on the row named n=. +grep -qE 'n="Cache Warm Path" sc="([^"]*::)?Orientation Guide"' "$MAP" \ && ok "Cache Warm Path is scoped under Orientation Guide" \ || no "Cache Warm Path carries no Orientation Guide scope (heading hierarchy missing)" -grep -qF '::Deployment Rollout Setext::Rollback Plan"' "$MAP" \ +grep -qE 'n="Rollback Plan" sc="([^"]*::)?Deployment Rollout Setext"' "$MAP" \ && ok "Rollback Plan (setext H2) is scoped under Deployment Rollout Setext (setext H1)" \ || no "setext hierarchy missing (Rollback Plan not scoped under the setext H1)" -grep -qF '::Rollback Plan::Deep Appendix"' "$MAP" \ +grep -qE 'n="Deep Appendix" sc="([^"]*::)?Rollback Plan"' "$MAP" \ && ok "Deep Appendix (H3) is scoped under Rollback Plan (the nearest shallower heading)" \ || no "Deep Appendix is not scoped under Rollback Plan" diff --git a/test/nestedqualcheck.sh b/test/nestedqualcheck.sh index ddc9e5e07..3fa0f0b89 100755 --- a/test/nestedqualcheck.sh +++ b/test/nestedqualcheck.sh @@ -115,7 +115,7 @@ fi || no "member scope invariance failed (see the constructor assertion above)" # ── (f) ROUTE SCOPE ──────────────────────────────────────────────────────────────────────────────── -"$BIN" nestedqualfix --for=Inner 2>/dev/null | grep -q 'routed: name-exact' \ +"$BIN" nestedqualfix --for=Inner 2>/dev/null | grep -q 'route="name-exact(' \ && ok "--for=Inner takes the name-exact route (the route this fix's ranking arms measure)" \ || no "--for=Inner no longer routes name-exact" diff --git a/test/notescheck.sh b/test/notescheck.sh index 5d33fd64f..79b9aec99 100755 --- a/test/notescheck.sh +++ b/test/notescheck.sh @@ -49,7 +49,20 @@ run(){ ( cd "$WORK" && "$BIN" . --no-cache "$@" 2>/dev/null ); } # them so the gate is independent of how ing spells the root prefix. MAP0="$( run )" FILE_TARGET="$( printf '%s' "$MAP0" | grep -oE '::sc::n +COMPUTE_ID="$( printf '%s' "$MAP0" | python3 -c ' +import re, sys +doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) +for f in re.finditer( r"]*>(.*?)", doc, re.S ): + for row in re.finditer( r"<[sd]\b([^>]*)>", f.group( 2 ) ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and ( scope is None or a[ "sc" ] == scope ): + print( f.group( 1 ) + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +for row in re.finditer( r"]*)>", doc ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and "p" in a and ( scope is None or a[ "sc" ] == scope ): + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +' compute )" if [ -n "$FILE_TARGET" ]; then ok "discovered file target: $FILE_TARGET"; else no "could not discover the a.cpp file path"; fi if [ -n "$COMPUTE_ID" ]; then ok "discovered scoped canonical id: $COMPUTE_ID"; else no "could not discover Widget::compute canonical id"; fi # D5: --note-add normalizes a target's path component to ROOT-RELATIVE on write, stripping any leading diff --git a/test/objcsniffcheck.sh b/test/objcsniffcheck.sh index e4b7532d5..5c1fd1a6c 100755 --- a/test/objcsniffcheck.sh +++ b/test/objcsniffcheck.sh @@ -41,7 +41,8 @@ echo "objcsniffcheck: BIN=$BIN FIX=$FIX" MAP="$( "$BIN" "$FIX" --no-cache 2>/dev/null )" # ── (a) the victim class of symbol: a struct METHOD in an anonymous namespace in a HEADER ─────────── -printf '%s' "$MAP" | grep -q 'id="cpp_mentions_objc.h::SniffVictim::sniffVictimMethod"' \ +# row 6 (2026-09-12): a scoped row prints n= then sc= (the short id); the canonical id composes as ::sc::n +printf '%s' "$MAP" | grep -q 'n="sniffVictimMethod" sc="SniffVictim"' \ && ok "(a) sniffVictimMethod indexed with its SniffVictim scope (the symbol class that vanished pre-74)" \ || { no "(a) sniffVictimMethod missing or unscoped — the comment-mention header is misrouted again"; printf '%s\n' "$MAP" | tr '>' '>\n' | grep 'cpp_mentions_objc' | head -6; } diff --git a/test/packcallersharecheck.sh b/test/packcallersharecheck.sh index 0830b3f50..f26e5c16a 100755 --- a/test/packcallersharecheck.sh +++ b/test/packcallersharecheck.sh @@ -213,7 +213,12 @@ fi # section grew by a few dozen bytes and the callers quota moved with it. Swept 1225..1400 on the new binary: # section omitted <= 1235, shown="1" on 1240..1270, shown="2" on 1275..1300, fully fit from 1320. 1255 sits # mid-window; the PROPERTY (one row admitted, two withheld, no bytes spent on an uninformative shared="1") is unchanged. -runB --pack-task="anchorSolo" --token-budget=1255 > "$TMP/b_tight.xml" +# RE-PINNED 1255 -> 1235 (2026-09-12, row 6: sc= on the rows — the path-repeating id= is gone, so the ranking +# section shrank by a few dozen bytes on this fixture and the callers quota moved with it, the same way P7's +# p= moved it the other way). Swept 1200..1340 on the new binary, step 5 then 2 at the edges: section omitted +# <= 1215, shown="1" on 1220..1250, shown="2" from 1252, fully fit from 1290. 1235 sits mid-window; the +# PROPERTY (one row admitted, two withheld, no bytes spent on an uninformative shared="1") is unchanged. +runB --pack-task="anchorSolo" --token-budget=1235 > "$TMP/b_tight.xml" TIGHT_TAG="$( grep -o ']*>' "$TMP/b_tight.xml" )" [ "$TIGHT_TAG" = '' ] \ && ok "arm (4b) capped-budget single-anchor callers: shown=\"1\"/total=\"3\"/capped=\"1\" unchanged from the pre-lane rendering" \ diff --git a/test/packtaskcheck.sh b/test/packtaskcheck.sh index ee42d2659..077b804f8 100755 --- a/test/packtaskcheck.sh +++ b/test/packtaskcheck.sh @@ -97,7 +97,20 @@ EOF runw(){ ( cd "$WORK" && "$BIN" . --no-cache "$@" 2>/dev/null ); } # discover the scoped canonical id EXACTLY as serialization spells it (id= is emitted only when scoped) -PID="$( runw | grep -oE 'id="[^"]*BudgetPlanner::parseBudget"' | head -1 | sed -E 's/id="([^"]*)"/\1/' )" +# row 6 (2026-09-12): the row prints the short id sc= beside its own p=; the canonical id composes as p::sc::n +PID="$( runw | python3 -c ' +import re, sys +doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) +for f in re.finditer( r"]*>(.*?)", doc, re.S ): + for row in re.finditer( r"<[sd]\b([^>]*)>", f.group( 2 ) ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and ( scope is None or a[ "sc" ] == scope ): + print( f.group( 1 ) + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +for row in re.finditer( r"]*)>", doc ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and "p" in a and ( scope is None or a[ "sc" ] == scope ): + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +' parseBudget BudgetPlanner )" if [ -n "$PID" ]; then ok "discovered scoped canonical id: $PID"; else no "could not discover BudgetPlanner::parseBudget id"; fi # D5: --note-add normalizes a target's path component to ROOT-RELATIVE on write (strips the crawl's leading # "./"), and --pack-task's notes section keys on that same normalized form — see notescheck.sh for the full diff --git a/test/packtaskquotacheck.sh b/test/packtaskquotacheck.sh index c0cd885b0..f35466ec8 100755 --- a/test/packtaskquotacheck.sh +++ b/test/packtaskquotacheck.sh @@ -77,7 +77,20 @@ EOF && git add -A && git commit -qm init >/dev/null 2>&1 ) runw(){ ( cd "$WORK" && "$BIN" . --no-cache "$@" 2>/dev/null ); } -PID="$( runw | grep -oE 'id="[^"]*BudgetPlanner::parseBudget"' | head -1 | sed -E 's/id="([^"]*)"/\1/' )" +# row 6 (2026-09-12): the row prints the short id sc= beside its own p=; the canonical id composes as p::sc::n +PID="$( runw | python3 -c ' +import re, sys +doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) +for f in re.finditer( r"]*>(.*?)", doc, re.S ): + for row in re.finditer( r"<[sd]\b([^>]*)>", f.group( 2 ) ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and ( scope is None or a[ "sc" ] == scope ): + print( f.group( 1 ) + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +for row in re.finditer( r"]*)>", doc ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and "p" in a and ( scope is None or a[ "sc" ] == scope ): + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +' parseBudget BudgetPlanner )" if [ -n "$PID" ]; then ok "discovered scoped canonical id: $PID"; else no "could not discover BudgetPlanner::parseBudget id"; fi runw --note-add="$PID: watch integer overflow when raw is INT_MAX" >/dev/null # D5 (see packtaskcheck.sh): --note-add normalizes the target's path segment to ROOT-RELATIVE on write diff --git a/test/pincensuscheck.sh b/test/pincensuscheck.sh index 75af65183..d7f679d80 100755 --- a/test/pincensuscheck.sh +++ b/test/pincensuscheck.sh @@ -49,14 +49,56 @@ echo "pincensuscheck: BIN=$BIN CORPUS=$CORPUS" # not raise `amb=`. It is the documented S6-C contract; a change that alters it must come with its own # registered justification, and this arm is where that shows up. MAP="$( "$BIN" "$CORPUS" --no-cache 2>/dev/null )" -RUN_ROW="$( printf '%s' "$MAP" | tr '<' '\n' | grep 'id="pinned.py::Alpha::run"' )" -GO_ROW="$( printf '%s' "$MAP" | tr '<' '\n' | grep 'id="tied.py::Eps::go"' )" + +# ── THE LOOKUP IS BOUND TO ITS FILE (PR #215 review, CodeRabbit 5191303552) ──────────────────────── +# Row 6 (2026-09-12) replaced the row's path-repeating id="PATH::SCOPE::NAME" with sc="SCOPE" alone, and +# these arms were re-keyed onto n= plus sc= over the WHOLE map. That reads the right row on this fixture +# by LUCK — Alpha exists only in pinned.py and Eps only in tied.py, so nothing else can match — but it +# stopped PROVING that Alpha::run belongs to pinned.py or Eps::go to tied.py, which is half of what the +# subject of this census is. The canonical id composes as p::sc::n with p= coming from the enclosing +# , so the lookup composes it the same way: the row must sit inside THAT file's block and carry +# THAT sc= and THAT n=. index() throughout, never a regex, so a '.' in a path is a '.'. +fileBlock() # $1=map $2=path -> that file's block, nothing else +{ + printf '%s' "$1" | awk -v p="$2" ' + BEGIN { RS = "<"; ORS = "" } + index( $0, "f p=\"" ) == 1 { inf = ( index( $0, "f p=\"" p "\"" ) == 1 ) } + inf { print "<" $0 } + index( $0, "/f>" ) == 1 { inf = 0 } + ' +} +fileScopedRow() # $1=map $2=path $3=scope $4=name -> the row(s) for p::sc::n, or empty +{ + fileBlock "$1" "$2" | awk -v sc="$3" -v n="$4" ' + BEGIN { RS = "<" } + index( $0, "s " ) == 1 && index( $0, " n=\"" n "\"" ) > 0 && index( $0, " sc=\"" sc "\"" ) > 0 { print } + ' +} +RUN_ROW="$( fileScopedRow "$MAP" "pinned.py" "Alpha" "run" )" +GO_ROW="$( fileScopedRow "$MAP" "tied.py" "Eps" "go" )" +[ "$( printf '%s\n' "$RUN_ROW" | grep -c 's ' )" = 1 ] \ + && ok "(A) pinned.py::Alpha::run resolves to exactly ONE row, p= sc= and n= all read" \ + || no "(A) pinned.py::Alpha::run does not resolve to one row inside : $RUN_ROW" +[ "$( printf '%s\n' "$GO_ROW" | grep -c 's ' )" = 1 ] \ + && ok "(A) tied.py::Eps::go resolves to exactly ONE row, p= sc= and n= all read" \ + || no "(A) tied.py::Eps::go does not resolve to one row inside : $GO_ROW" +# …and the CONTROL that makes those two arms mean something: the same lookup with the WRONG p= must +# find NOTHING. A whole-map grep on n=/sc= passes this fixture and can never fail it, which is exactly +# why it stopped being evidence. If the composition ever loses its p= half, these two go red. +[ -z "$( fileScopedRow "$MAP" "tied.py" "Alpha" "run" )" ] \ + && ok "(A) control: Alpha::run is NOT found under p=\"tied.py\" — the arm reads the path, not the name alone" \ + || no "(A) control: Alpha::run matched under the WRONG path — this lookup is not reading p=" +[ -z "$( fileScopedRow "$MAP" "pinned.py" "Eps" "go" )" ] \ + && ok "(A) control: Eps::go is NOT found under p=\"pinned.py\"" \ + || no "(A) control: Eps::go matched under the WRONG path — this lookup is not reading p=" + if printf '%s' "$RUN_ROW" | grep -q 'amb='; then no "(A) pinned.py::Alpha::run carries amb= — the locality pin is no longer silent: $RUN_ROW" else ok "(A) the locality pin is SILENT — pinned.py::Alpha::run carries no amb=" fi -N_HELPER="$( printf '%s' "$MAP" | tr '>' '\n' | awk '/id="pinned.py::Alpha::run"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="helper"' )" +# the edge walk starts from that same file-bound row, not from the first n="run" anywhere in the map +N_HELPER="$( fileBlock "$MAP" "pinned.py" | tr '>' '\n' | awk '/n="run" sc="Alpha"/{f=1} f{print} /\/s/{if(f)exit}' | grep -c 'n="helper"' )" [ "$N_HELPER" = 1 ] && ok "(A) the pin emitted ONE confident edge (not a split)" \ || no "(A) pinned.py::Alpha::run emitted $N_HELPER helper edges, want 1" printf '%s' "$GO_ROW" | grep -q 'amb="1"' && ok "(A) the tied control is HONEST — tied.py::Eps::go carries amb=\"1\"" \ diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index fa37dbca2..76486d61a 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -1,4 +1,4 @@ -flagless 0 4fbbea0a8d22d1633cc7b0469ffbe75cd3775c0065c8982015a539ad5d08a06e e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +flagless 0 0553422259c2f203b0819ab4bb8a769d31b244360a6e7d81851ab5dbf0591e1c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint 0 bd805656a4ccad157cbac04128866b4d2eecdaec891a5b0d3fd6af6118f77256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_sarif 0 805e375b06b7755bcc6423607378e557ee82fef787cf09c34ba5c066639f4d7b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_select 0 156ac3dbd4bd5d184e7182ce412ec57dd1886fadc76c0df7378dd51e556f9b33 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -10,15 +10,15 @@ callers 0 c8317dc738f05616369995c0a0c03a6c12fdea8d2a2c5b7d182156a4de76cdc6 e3b0c impact 0 4009dd7227dabc63a96e6318e77c6f704f25b7c2a31295fa80510b1df48b6e85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 69e768ecb0be2645c1848a99a3b6f541a0a37dbcfb7383d96a5cc90c636fe792 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help 0 aee3ae7d926954108ded89973f359725523423877a55c2c1db3a5909d6740d0f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -expand 0 031a0bef5b4dbd5f548ce89d76928b132b7249b26ab540b1896d19e9f2c7d29d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +expand 0 f06c70abc1d5c8a941bd340adc08173232e83790ce024a14d20e1bcba5d2808d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 69470d30c1abfb98cebbabf5d62f502822cd8a0cb44dee8de42b2ba3a6349ec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -around 0 94ebc7f1baf91c0577f2b69310bc9129dc0dcb8dea8760fdf4bb5babe20cdba8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +around 0 1f3efff7030b4a0a13e44736476bb920c3ae911559dd940b728e8ef5d985dc82 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 uses 0 e8eac2b7d6798ad1166bb3f6d767436ca4689c0b26f8555e147380ea8e0615a5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 path 0 7058d89f7bab7aabe0a5cbf921959bc8a2c346fe65888b776996f879f04a58a2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connect 0 31c7e3a689a6dcddf5eae17283740823005c3efdfdf3945be5770b2660a152b7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 b171aa1e5c28b47827f9148c2a5fc6948fb22938d3da21e6551b12432f492472 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_signatures 0 4a22aeda1d2e36fd390065dac1c07955e9f8942e7ba858435281863653f373ba e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_task 0 b4a88debacae6771a3c7e050fe0ed882c33e40faf80afd056e843a0e5095fe75 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_signatures 0 0d79e8f31f99dc1936a25b016b447ea8dcae12cad984067e6f2e3d70378a61ae e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_task 0 d8b0cd1cce360f40af454f28c1c31c3dfa9003a366a77029af1f46b577bc7857 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 6014e2f18ba2d587f70d59fcbe62f64c38e8c556291006ae3dfd76ef7259315a e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 23b824f67984709f3c01dc7cc97c4c682b2afbaa5b63677a79ce56a1a58e6454 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 skipped 0 5b9749fdafec842ae75c6ad07e9eac8eb435653f8e5fc1ce452853fc36ff96f2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 b06980d52e4991e57563059d8be986bb4614779e20ccfd3790aac1ee076d2512 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 ab9dee52240f70055fa4d82d6b928ef52f4f5781c6a80ee39edb49805b892719 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 da9c5c95f0a34b492bd06c47652a0c616443fc1623552853c876efb8846264c8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 87024074d340a6ac2d44f3a70db5c99d037446490e042517ab24b1bd86182429 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/qualitykeycheck.sh b/test/qualitykeycheck.sh index 40c204207..2cdf81091 100755 --- a/test/qualitykeycheck.sh +++ b/test/qualitykeycheck.sh @@ -210,7 +210,20 @@ fi D="$( newrepo replay )" ( cd "$D" && scopedSimple > w.cpp && git add -A && git commit -qm base ) ( cd "$D" && scopedComplex > w.cpp ) -CANON="$( cd "$D" && "$BIN" . --top-k=50 2>/dev/null | tr '>' '>\n' | sed -n 's/.*n="compute" id="\([^"]*\)".*/\1/p' | head -1 )" +# row 6 (2026-09-12): the row prints the short id sc=; the canonical id composes as ::sc::n +CANON="$( cd "$D" && "$BIN" . --top-k=50 2>/dev/null | python3 -c ' +import re, sys +doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) +for f in re.finditer( r"]*>(.*?)", doc, re.S ): + for row in re.finditer( r"<[sd]\b([^>]*)>", f.group( 2 ) ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and ( scope is None or a[ "sc" ] == scope ): + print( f.group( 1 ) + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +for row in re.finditer( r"]*)>", doc ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and "p" in a and ( scope is None or a[ "sc" ] == scope ): + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +' compute )" if [ -z "$CANON" ]; then no "(C) could not read the emitted canonical id for Widget::compute" else @@ -258,7 +271,7 @@ D="$( newrepo g4 )" ( cd "$D" && simpleA > a.cpp ) row="$( "$BIN" "$D" --top-k=50 2>/dev/null | tr '>' '>\n' | grep -o ']*n="helper"[^>]*' | head -1 )" case "$row" in - *' id="'*) no "(F) a scope-less symbol grew an id= attribute — canonicalId moved, and that is the +26.4% G4 breach: $row" ;; + *' sc="'*|*' id="'*) no "(F) a scope-less symbol grew an sc=/id= attribute — canonicalId moved, and that is the +26.4% G4 breach: $row" ;; '') no "(F) could not find the helper row on the default map" ;; *) ok "(F) scope-less symbols still emit no id= — canonicalId untouched, map density preserved" ;; esac diff --git a/test/regression.sh b/test/regression.sh index 1a0173aad..be124e541 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -268,7 +268,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck declinedlistcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck decltodefcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck stdqualcheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck declinedlistcheck scroundtripcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/routecheck.sh b/test/routecheck.sh index c4c89ee46..dadb163aa 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -11,6 +11,11 @@ # (a) SAFE FALLBACK — a CONCEPTUAL --for query defaults to subtoken+body; its RANKING is byte-identical to # the pre-routing golden captured via --no-route (the confidence gate does not over-fire on prose). # (b) identifier query — --for="buildGraph" (DEFAULT, no flag) routes to name-exact; the header says so. +# NOT RE-PINNED 2026-09-13 (lane/sc-legend, PR #215): the sc= reading joined this legend (+29 B) and then left it +# again when the review round made both identity readings present-only -- routefix serves free functions, which +# have no enclosing scope, so there is nothing for the reading to define. Byte-identical to its pre-lane self at +# 3,776 B. The golden is also the --no-route capture, so the route= reading is absent: both halves of the +# present-only rule are controlled here. # (c) --no-route forces subtoken+body and matches the pre-flip capture byte-for-byte (golden neutrality # preserved for the opt-out path); its header carries NO 'routed:' note. # (d) determinism — two DEFAULT --route runs are byte-identical. @@ -112,28 +117,28 @@ diff -q "$TMP/concept_noroute.xml" "$ROOT/test/routefix/golden_for.xml" >/dev/nu || no "conceptual --for --no-route drifted from test/routefix/golden_for.xml" # the DEFAULT (routed) conceptual run must fall back to subtoken+body — same ranker, only a header note added. "$BIN" routefix --no-cache --for="$CONCEPT" >"$TMP/concept_default.xml" 2>/dev/null -grep -q 'routed: subtoken+body' "$TMP/concept_default.xml" \ +grep -q 'route="subtoken+body' "$TMP/concept_default.xml" \ && ok "safe fallback: conceptual --for DEFAULTS to subtoken+body (no over-fire to name-exact)" \ || no "conceptual --for did not fall back to subtoken+body (router over-fired on prose)" # ── (b) identifier query DEFAULTS to name-exact (routing is on with no flag) ─────────────────────────── "$BIN" routefix --no-cache --for="buildGraph" >"$TMP/ident.xml" 2>/dev/null -grep -q 'routed: name-exact' "$TMP/ident.xml" \ +grep -q 'route="name-exact(' "$TMP/ident.xml" \ && ok "identifier query 'buildGraph' DEFAULTS to name-exact BM25 (routing is on by default)" \ - || no "identifier query did not route to name-exact (header missing 'routed: name-exact')" + || no "identifier query did not route to name-exact (root missing route=\"name-exact(\")" # A7: an identifier embedded in LONG issue/review prose is evidence, not the whole intent. The old # any-camel/snake rule discarded every prose/body term and cratered corrected LocBench train retrieval. "$BIN" routefix --no-cache --for="repair buildGraph when the serialized ranked map is empty after cache reload" >"$TMP/long_ident.xml" 2>/dev/null -grep -q 'routed: subtoken+body' "$TMP/long_ident.xml" \ +grep -q 'route="subtoken+body' "$TMP/long_ident.xml" \ && ok "long issue prose with one identifier stays subtoken+body" \ || no "one identifier over-fired name-exact on a long conceptual query" # ── (c) --no-route forces subtoken+body and matches the pre-flip capture; header carries NO routed note ─ "$BIN" routefix --no-cache --for="buildGraph" --no-route >"$TMP/ident_noroute.xml" 2>/dev/null -{ ! grep -q 'routed:' "$TMP/ident_noroute.xml"; } \ - && ok "--no-route on an identifier query forces subtoken+body (no 'routed:' header note)" \ - || no "--no-route still emitted a 'routed:' note — the opt-out did not disable routing" +{ ! grep -q ' route="' "$TMP/ident_noroute.xml"; } \ + && ok "--no-route on an identifier query forces subtoken+body (no route= attribute)" \ + || no "--no-route still emitted a route= attribute — the opt-out did not disable routing" # ── (d) determinism — two DEFAULT --for runs byte-identical ──────────────────────────────────────────── "$BIN" routefix --no-cache --for="buildGraph" >"$TMP/r1" 2>/dev/null @@ -168,7 +173,7 @@ fi # reconstructed from what the assertion expects. # verify-wave2 F6 re-pin: stop on the attribute quote. The trailing "]" this used to anchor on was the # unbalanced half of a bracket pair L10b half-trimmed; route= is delimited by its own quotes. -reasonOf(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE 'routed: [^"]*' | head -1; } +reasonOf(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE ' route="[^"]*' | head -1 | sed 's/^ route="//'; } routeOf(){ reasonOf "$@" | grep -oE 'name-exact|subtoken\+body' | head -1; } # (f1) the fixture's identifier query: buildGraph is defined once, in routefix/graph.cpp. @@ -240,8 +245,8 @@ esac # (f5) the disclosure rides inside the EXISTING reason: the phrase downstream gates read must survive. case "$identReason" in - *'names a symbol (buildGraph)'*) ok "(f5) the pre-existing 'names a symbol (X)' phrasing is intact — the anchors were appended, not substituted" ;; - *) no "(f5) the anchors replaced the existing reason phrasing; test/taskechocheck.sh reads 'names a symbol (…)' out of this same string: [$identReason]" ;; + *'name-exact(buildGraph)'*) ok "(f5) the ranker code name-exact(X) is intact — the anchors were appended, not substituted (row 6: the code replaced the 'names a symbol (X)' prose; test/taskechocheck.sh reads the code out of this same string)" ;; + *) no "(f5) the anchors replaced the ranker code; test/taskechocheck.sh reads 'name-exact(…)' out of this same string: [$identReason]" ;; esac # ── (g) ANCHOR PLAUSIBILITY (LB-2): the all-words trigger at nWords>=2 additionally requires every @@ -298,14 +303,14 @@ else fi # NOTE the quotes around the anchor word arrive attribute-escaped (') — match on the words, not the quotes. case "$declinedReason" in - *"name-exact declined: anchor"*"split"*"name-carriers"*"defs"*) + *"subtoken+body:declined("*"split"*"-carriers,"*"-defs)"*) ok "(g1) the declined reason names the failing anchor and its carrier count: [$declinedReason]" ;; *) no "(g1) the declined reason must say WHY (failing anchor + carrier count) — got: [$declinedReason]" ;; esac case "$declinedReason" in - *"anchors:"*|*"names a symbol ("*) - no "(g1) a declined (subtoken+body) reason carried a name-exact-only literal ('anchors:' / 'names a symbol (') — downstream gates parse those as name-exact markers: [$declinedReason]" ;; - *) ok "(g1) the declined reason carries neither 'anchors:' nor 'names a symbol ('" ;; + *"anchors:"*|*"name-exact("*) + no "(g1) a declined (subtoken+body) reason carried a name-exact-only literal ('anchors:' / 'name-exact(') — downstream gates parse those as name-exact markers: [$declinedReason]" ;; + *) ok "(g1) the declined reason carries neither 'anchors:' nor 'name-exact('" ;; esac # (g2) the decline is a RECOVERY, not a shrug: the conceptual ranking surfaces the compound target. @@ -340,12 +345,14 @@ fi # (g6) MUTATION arm — the declined-disclosure assertion must FAIL against a --no-route run of the same # query (no routed: note at all there), proving the assertion is live and reads real output. "$BIN" commonfix --no-cache --for="split chunks" --no-route >"$TMP/declined_noroute.xml" 2>/dev/null -GMUT="$( grep -q 'name-exact declined' "$TMP/declined_noroute.xml" && echo BAD || echo TRIPPED )" +# row 6: read the route ATTRIBUTE only — the legend's route= reading spells ':declined(' on every --for document, +# routed or not, so a whole-document grep would never trip and the self-test would be shape 5 (no contrast). +GMUT="$( grep -oE ' route="[^"]*"' "$TMP/declined_noroute.xml" | grep -q ':declined(' && echo BAD || echo TRIPPED )" [ "$GMUT" = "TRIPPED" ] && ok "(g6) mutation self-test (the declined assertion fails on the --no-route run, so it is live)" \ || no "(g6) mutation self-test broke — the declined assertion cannot fail" # ── MUTATION self-test — the name-exact routing assertion must FAIL against the --no-route run ───────── -MUT="$( grep -q 'routed: name-exact' "$TMP/ident_noroute.xml" && echo BAD || echo TRIPPED )" +MUT="$( grep -q 'route="name-exact(' "$TMP/ident_noroute.xml" && echo BAD || echo TRIPPED )" [ "$MUT" = "TRIPPED" ] && ok "mutation self-test (the routing assertion fails on the --no-route run, so it is live)" \ || no "mutation self-test broke — the routing assertion cannot fail" diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 7317305fd..df8103a7f 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -266,6 +266,24 @@ observe "$( bashcall adopt3 'ripwire . --expand=targetSymbol' )" "$H9" && ok "O3 observe: a third call is OUTSIDE the window — the second non-adoption closes it as missed" \ || no "O3 observe: rows=$( rows "$H9/routing.jsonl" ) third=[$( rowget "$H9/routing.jsonl" 3 outcome )]" +# O8 (2026-09-12, the local routing analysis's instrument bug): a DIRECTORY named ripwire is not a ripwire call. +# `cd /x/ripwire && git log --oneline` carries a token ending in /ripwire in ARGUMENT position; the observer +# counted it as a call, burned a window slot, and a real adoption two commands later read as `missed`. Only the +# COMMAND word counts — `ripwire`, `./build/ripwire`, any path whose basename is ripwire in command position +# (after ^ ; & | ( or $( and any leading VAR=value assignments) — never an argument. +H13="$TMP/h13"; mkdir -p "$H13" +route_run "$H13" "$WITH_RIPWIRE" "$( promptjson adopt7 "$REPO" "$RECPROMPT" )" RIPWIRE_METER_ARM=treatment >/dev/null 2>&1 +observe "$( bashcall adopt7 'cd /tmp/x/ripwire && git log --oneline' )" "$H13" +observe "$( bashcall adopt7 'ls -la /opt/ripwire' )" "$H13" +observe "$( bashcall adopt7 'echo ripwire' )" "$H13" +[ "$( rows "$H13/routing.jsonl" )" = 1 ] \ + && ok "O8 observe: a directory argument ending in /ripwire (and a bare word in argument position) consumes no window slot" \ + || no "O8 observe: a non-call consumed a window slot — rows=$( rows "$H13/routing.jsonl" ) (want 1: the prompt row only)" +observe "$( bashcall adopt7 'cd /tmp/x/repo && ./build/ripwire . --expand=targetSymbol' )" "$H13" +[ "$( rowget "$H13/routing.jsonl" 2 outcome )" = adopted ] && [ "$( rowget "$H13/routing.jsonl" 2 position )" = 1 ] \ + && ok "O8 observe: the real call after them (a path in command position after &&) still adopts at position 1" \ + || no "O8 observe: row 2 = outcome=[$( rowget "$H13/routing.jsonl" 2 outcome )] position=[$( rowget "$H13/routing.jsonl" 2 position )] (want adopted at 1)" + # The CONTROL arm's adoption is observed identically. Without this the band has one arm. H10="$TMP/h10"; mkdir -p "$H10" route_run "$H10" "$WITH_RIPWIRE" "$( promptjson adopt4 "$REPO" "$RECPROMPT" )" RIPWIRE_METER_ARM=control >/dev/null 2>&1 @@ -481,5 +499,79 @@ done && ok "P1 docs: EVALS.md carries the pre-registered metric and band this hook is the instrument for" \ || no "P1 docs: EVALS.md is missing:$PMISS" + +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# O9 — rw_is_ripwire_call: ONE block, three files, and the shapes an agent actually types (PR #215 item 6) +# ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# The regex this replaced said NO to every WRAPPED invocation (`time ./build/ripwire`, `sudo`, `env X=1`, +# `xargs`, `exec`, `nohup`, `if ripwire`, `{ ripwire`) and YES to a quoted string in a git message. Worse, the +# nudge hook's own meter kept a THIRD opinion, so the two counts whose ratio is the published substitution rate +# were taken with different instruments. The block is mirrored; these arms assert the copies are identical +# (the kIngestParserVerMirror pattern) and that the rule answers each shape correctly. +echo +echo "=== O9: the command-word rule is one text in three files, and reads the wrapped shapes ===" +extract_block(){ awk '/^# ---- BEGIN MIRRORED BLOCK rw_is_ripwire_call/,/^# ---- END MIRRORED BLOCK rw_is_ripwire_call/' "$1"; } +B_CLAUDE="$TMP/b_claude.sh"; B_CODEX="$TMP/b_codex.sh"; B_NUDGE="$TMP/b_nudge.sh" +extract_block "$ROOT/hooks/ripwire-claude-route.sh" > "$B_CLAUDE" +extract_block "$ROOT/hooks/ripwire-codex-route.sh" > "$B_CODEX" +extract_block "$ROOT/hooks/ripwire-nudge.sh" > "$B_NUDGE" +if [ ! -s "$B_CLAUDE" ]; then + no "O9 the mirrored block is absent from hooks/ripwire-claude-route.sh (the arm below would prove nothing)" +elif diff -q "$B_CLAUDE" "$B_CODEX" >/dev/null && diff -q "$B_CLAUDE" "$B_NUDGE" >/dev/null; then + ok "O9 rw_is_ripwire_call is byte-identical in the three hooks ($( wc -l < "$B_CLAUDE" | tr -d ' ' ) lines)" +else + no "O9 the three copies of rw_is_ripwire_call have DRIFTED — the hooks and the meter will disagree on the same command line" +fi + +# The rule itself, sourced from the claude hook's copy so the arm tests what ships. +cat "$B_CLAUDE" > "$TMP/rule.sh" +printf 'if rw_is_ripwire_call "$1"; then echo 1; else echo 0; fi\n' >> "$TMP/rule.sh" +o9_bad=0; o9_n=0 +o9(){ # $1 = expected (1/0), $2 = command line + o9_n=$(( o9_n + 1 )) + got="$( sh "$TMP/rule.sh" "$2" 2>/dev/null )" + [ "$got" = "$1" ] || { o9_bad=$(( o9_bad + 1 )); printf ' want=%s got=%s %s\n' "$1" "$got" "$2"; } +} +# CALLS — every one of these runs ripwire, and every one of them read as "not a call" before this change +o9 1 'ripwire . --for=x' +o9 1 './build/ripwire .' +o9 1 'time ./build/ripwire .' +o9 1 'sudo ripwire .' +o9 1 'env RIPWIRE_BIN=x ripwire .' +o9 1 'xargs ripwire' +o9 1 'exec ripwire .' +o9 1 'nohup ripwire . &' +o9 1 'if ripwire . --for=x; then echo y; fi' +o9 1 '{ ripwire . ; }' +o9 1 'cd /tmp && ripwire .' +o9 1 'rtk proxy ripwire .' +o9 1 'git log --oneline && ripwire .' +# ROUND 2 (CodeRabbit, PR #215): a CONTROL OPERATOR ATTACHED TO A WORD. The rule used to ask the shell to +# split the line, and word splitting does not lex operators: `true; ripwire .` split into `true;` + `ripwire`, +# `true;` read as an ordinary command word, and the call behind it was no longer in command position. RED on +# the round-1 block, measured while writing this: all five of these answered 0. +o9 1 'true; ripwire .' +o9 1 'echo hi;ripwire .' +o9 1 'false||ripwire .' +o9 1 '(ripwire .)' +o9 1 'git log --oneline&&ripwire .' +o9 1 'ripwire . 2>&1 | head' +# NOT CALLS — the word appears, nothing runs +o9 0 'git commit -m "fix; ripwire hook"' +o9 0 'cd /opt/src/ripwire && git log --oneline' +o9 0 'ls /opt/ripwire' +o9 0 'grep -r ripwire src/' +o9 0 'echo ripwire' +# …and the three the lexer answers for a REASON rather than by accident: a `;` inside quotes is not an +# operator (round 1 got this right only because `-m "fix;` happened not to end a command), a `#` at the start +# of a word begins a comment, and a redirection TARGET is not a command word. +o9 0 "grep -r 'ripwire;' src/" +o9 0 'echo "a; ripwire b"' +o9 0 '# ripwire .' +o9 0 'echo hi > ripwire' +[ "$o9_bad" -eq 0 ] \ + && ok "O9 command-word rule: $o9_n shapes read correctly (19 wrapped/sequenced/operator-attached calls, 9 appearances that run nothing)" \ + || no "O9 command-word rule: $o9_bad of $o9_n shapes read WRONG (listed above)" + echo if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi diff --git a/test/routeoncecheck.sh b/test/routeoncecheck.sh index 11e6cb5d0..3fef3a52e 100755 --- a/test/routeoncecheck.sh +++ b/test/routeoncecheck.sh @@ -37,8 +37,10 @@ cd "$TMP" # (a) --for, routed: the disclosure text appears exactly once, and the attribute carries it. "$BIN" fix --for="buildGraph" --no-cache >"$TMP/for.xml" 2>/dev/null -n="$( grep -o 'routed:' "$TMP/for.xml" | wc -l | tr -d ' ' )" -if [ "$n" = 1 ]; then ok "--for emits 'routed:' exactly once (got $n)"; else no "--for emits 'routed:' $n times (want exactly 1 — the route= attribute)"; fi +# row 6 (2026-09-12): the disclosure is a CODE now (name-exact(X) / subtoken+body…), no "routed:" prose — so the +# once-and-only-once property is read off the attribute itself: exactly one route= on the document. +n="$( grep -o ' route="' "$TMP/for.xml" | wc -l | tr -d ' ' )" +if [ "$n" = 1 ]; then ok "--for emits route= exactly once (got $n)"; else no "--for emits route= $n times (want exactly 1 — the root attribute)"; fi grep -q 'route="' "$TMP/for.xml" \ && ok "--for keeps the route= attribute (the machine-readable copy)" \ || no "--for lost the route= attribute — the surviving copy must be the ATTRIBUTE, not the comment" @@ -57,9 +59,9 @@ grep -oE 'route="[^"]*"' "$TMP/for.xml" | grep -q '^route=" \[' \ grep -oE 'route="[^"]*"' "$TMP/for.xml" | grep -q ']"$' \ && no "--for route= still ends with a stray unbalanced bracket" \ || ok "--for route= ends with the route reason, not a bracket" -grep -oE 'route="routed: ' "$TMP/for.xml" >/dev/null \ - && ok "--for route= starts directly with 'routed: ' (no leading filler)" \ - || no "--for route= does not start with 'routed: ' — the trim moved the anchor, not just the space" +grep -oE 'route="(name-exact\(|subtoken\+body)' "$TMP/for.xml" >/dev/null \ + && ok "--for route= starts directly with the ranker code (no leading filler)" \ + || no "--for route= does not start with a ranker code (name-exact( / subtoken+body) — the trim moved the anchor, not just the space" # (a2) --for on the CONCEPTUAL route (2026-08-23 serving-shape sweep): the same single-copy contract on # the COMPACT serving shape. (a)'s query anchors, so it pins the contract only on the auto body walk; @@ -70,16 +72,16 @@ grep -oE 'route="routed: ' "$TMP/for.xml" >/dev/null \ grep -q 'bundle="compact"' "$TMP/forc.xml" \ && ok "--for conceptual presence: the query serves the COMPACT shape" \ || no "--for conceptual presence: the query no longer serves bundle=\"compact\" — re-author it, the compact arm observes the wrong shape" -n="$( grep -o 'routed:' "$TMP/forc.xml" | wc -l | tr -d ' ' )" -if [ "$n" = 1 ]; then ok "--for (compact shape) emits 'routed:' exactly once (got $n)"; else no "--for (compact shape) emits 'routed:' $n times (want exactly 1 — the route= attribute)"; fi +n="$( grep -o ' route="' "$TMP/forc.xml" | wc -l | tr -d ' ' )" +if [ "$n" = 1 ]; then ok "--for (compact shape) emits route= exactly once (got $n)"; else no "--for (compact shape) emits route= $n times (want exactly 1 — the root attribute)"; fi grep -q 'route="' "$TMP/forc.xml" \ && ok "--for (compact shape) keeps the route= attribute" \ || no "--for (compact shape) lost the route= attribute" # (b) --pack-task, same contract. "$BIN" fix --pack-task="buildGraph" --no-cache >"$TMP/pt.xml" 2>/dev/null -n="$( grep -o 'routed:' "$TMP/pt.xml" | wc -l | tr -d ' ' )" -if [ "$n" = 1 ]; then ok "--pack-task emits 'routed:' exactly once (got $n)"; else no "--pack-task emits 'routed:' $n times (want exactly 1)"; fi +n="$( grep -o ' route="' "$TMP/pt.xml" | wc -l | tr -d ' ' )" +if [ "$n" = 1 ]; then ok "--pack-task emits route= exactly once (got $n)"; else no "--pack-task emits route= $n times (want exactly 1)"; fi grep -q 'route="' "$TMP/pt.xml" \ && ok "--pack-task keeps the route= attribute" \ || no "--pack-task lost the route= attribute" diff --git a/test/rubyscopecheck.sh b/test/rubyscopecheck.sh index 3cb9a133e..cde09aa66 100755 --- a/test/rubyscopecheck.sh +++ b/test/rubyscopecheck.sh @@ -98,7 +98,10 @@ ROWS="$DIR/rows" sed 's/>\n"$ROWS" hasId(){ # $1=id $2=label - if grep -q "id=\"$1\"" "$ROWS"; then ok "$2 → id=\"$1\""; else no "$2 → id=\"$1\" MISSING: $( grep -o "n=\"${1##*::}\"[^>]*" "$ROWS" | head -2 | tr '\n' ' ' )"; fi + # row 6 (2026-09-12): the row prints n= then sc= (the enclosing scope); the canonical id $1 composes as p::sc::n, + # so "file::Scope::name" is read as n="name" sc="Scope" (the file half is the row's own ). + _name="${1##*::}"; _rest="${1#*::}"; _scope="${_rest%::*}" + if grep -q "n=\"$_name\" sc=\"$_scope\"" "$ROWS"; then ok "$2 → sc=\"$_scope\" (id $1)"; else no "$2 → n=\"$_name\" sc=\"$_scope\" (id $1) MISSING: $( grep -o "n=\"$_name\"[^>]*" "$ROWS" | head -2 | tr '\n' ' ' )"; fi } echo "=== ids: every Ruby def inside a class/module carries path::scope::name ===" @@ -114,8 +117,8 @@ hasId "t.py::B::__init__" "Python control: __init__ inside class B (unchanged echo "=== negative controls ===" TOP="$( grep 'n="toplevel"' "$ROWS" )" if [ -n "$TOP" ]; then ok "top-level def toplevel indexed"; else no "top-level def toplevel missing"; fi -echo "$TOP" | grep -q ' id="' && no "top-level def carries an id= (the file is not a scope): $TOP" || ok "top-level def carries NO id= (no scope)" -grep -q 'n="Outer" id=' "$ROWS" && no "top-level module Outer carries an id= (nothing encloses it)" || ok "top-level module Outer carries NO id=" +echo "$TOP" | grep -q ' sc="' && no "top-level def carries an sc= (the file is not a scope): $TOP" || ok "top-level def carries NO sc= (no scope)" +grep -q 'n="Outer" sc=' "$ROWS" && no "top-level module Outer carries an sc= (nothing encloses it)" || ok "top-level module Outer carries NO sc=" echo "=== overload folding: same name in DIFFERENT classes must stay separate rows ===" N_INIT="$( grep -c 'n="initialize"' "$ROWS" )" @@ -160,7 +163,7 @@ RUBY DMAP="$( "$BIN" "$DEL" --no-cache 2>/dev/null | sed 's/>\n]*edges=[0-9]*' | grep -o 'edges=[0-9]*' | cut -d= -f2 )" if [ "${DEDGES:-0}" -eq 2 ]; then ok "facade delegation keeps both real targets (edges=2)"; else no "facade delegation: expected edges=2, got ${DEDGES:-0} — the caller won its own locality tie-break?"; fi -FAC="$( echo "$DMAP" | awk '/id="n.rb::Facade::publish_event"/{f=1;print;next} /^\n\n/dev/null | sed 's/>\n/dev/null 2>&1 && { if xmllint --noout "$MAP"; then ok "xmllint --noout"; else no "xmllint failed"; fi; } SPLIT="$DIR/split"; sed 's/>\n"$SPLIT" -rowOf(){ awk -v pat="$1" '$0 ~ pat{f=1;print;next} /^ row matching an attribute + its children (top-level defs carry no id=, so pass n="…") +rowOf(){ awk -v pat="$1" '$0 ~ pat{f=1;print;next} /^ row matching an attribute + its children (top-level defs carry no sc=, so pass n="…") echo "=== setter definitions are indexed, scoped, and distinct from their getters ===" -if grep -q ']*' "$SPLIT" | head -3 | tr '\n' ' ' )"; fi -if grep -q 'n="count=" id="s.rb::W::count="' "$SPLIT"; then ok "def count=(v) → W::count="; else no "def count=(v) not indexed"; fi -if grep -q 'n="limit=" id="s.rb::W::limit="' "$SPLIT"; then ok "def self.limit=(v) → W::limit= (singleton setter)"; else no "def self.limit=(v) not indexed"; fi -if grep -q 'n="name=" id="s.rb::Other::name="' "$SPLIT"; then ok "Other#name= indexed separately"; else no "Other#name= not indexed"; fi -if grep -q ']*' "$SPLIT" | head -3 | tr '\n' ' ' )"; fi +if grep -q 'n="count=" sc="W"' "$SPLIT"; then ok "def count=(v) → W::count="; else no "def count=(v) not indexed"; fi +if grep -q 'n="limit=" sc="W"' "$SPLIT"; then ok "def self.limit=(v) → W::limit= (singleton setter)"; else no "def self.limit=(v) not indexed"; fi +if grep -q 'n="name=" sc="Other"' "$SPLIT"; then ok "Other#name= indexed separately"; else no "Other#name= not indexed"; fi +if grep -q '\|\| sum row — \`local 0\` did not bind across files" fi if grep -q 'prov="scip"' "$TMP/scip.xml"; then ok "(C) the map still carries prov=\"scip\" edges (the control pinned)"; else no "(C) no prov=\"scip\" edge at all"; fi -N_SUM_EDGE="$( tr '>' '\n' <"$TMP/scip.xml" | awk '/id="b.py::K::run"/{f=1} f&&/n="sum"/{c++} /\/s/{if(f)exit} END{print c+0}' )" +N_SUM_EDGE="$( tr '>' '\n' <"$TMP/scip.xml" | awk '/n="run" sc="K"/{f=1} f&&/n="sum"/{c++} /\/s/{if(f)exit} END{print c+0}' )" # row 6: n=/sc= is the row's identity now if [ "$N_SUM_EDGE" = 0 ]; then ok "(C) K.run emits no phantom edge to sum"; else no "(C) K.run carries $N_SUM_EDGE phantom sum edge(s) from the local collision"; fi # ── (D) the PARAMETER trap: a parameter never binds its function; the call is a NON-DEF resolution ─ @@ -77,7 +77,7 @@ EXT_ROW="$( awk -F'\t' '$1=="O" && $2 ~ /^a\.py::Box::total#/ && $3=="sum" {prin [ "$EXT_ROW" = "@external" ] && ok "(E) O row Box.total -> sum carries the @external sentinel (SCIP: builtins/sum)" \ || no "(E) Box.total -> sum O row target is '${EXT_ROW:-}', want @external" # the sentinel is census-only: the shipped map must NOT drop or replace the name-based edge for it -tr '>' '\n' <"$TMP/scip.xml" | awk '/id="a.py::Box::total"/{f=1} f&&/n="sum"/{c++} /\/s/{if(f)exit} END{exit !(c==1)}' \ +tr '>' '\n' <"$TMP/scip.xml" | awk '/n="total" sc="Box"/{f=1} f&&/n="sum"/{c++} /\/s/{if(f)exit} END{exit !(c==1)}' \ && ok "(E) the map keeps Box.total's name-based sum edge (a sentinel never edits the graph)" \ || no "(E) Box.total's sum edge changed under --scip" grep -c 'prov="scip"' "$TMP/scip.xml" >/dev/null diff --git a/test/scroundtripcheck.sh b/test/scroundtripcheck.sh new file mode 100755 index 000000000..f824e7e3c --- /dev/null +++ b/test/scroundtripcheck.sh @@ -0,0 +1,206 @@ +#!/usr/bin/env bash +# scroundtripcheck.sh — a symbol row's short id (sc=) composes to EXACTLY the id= it replaced, and that id +# still resolves. +# +# THE DEFECT. On every scoped map row the canonical id repeated the enclosing path verbatim +# (id="qual.cpp::Widget::ping" under ): 942 of 942 scoped rows on this tree, 11.2% of a +# flagless map (PLAN_OUTPUT_ROUTING_LOOP 2026-09-12, row 6). The row now prints sc="Widget" — the enclosing +# scope alone — and the legend states the composition: id = p::sc::n, p= taken from the row's own p= (a +# lens row) or from its enclosing (a map row). The selectors (--expand, --callers, --impact, +# --uses, the MCP twins) keep ACCEPTING the composed path::scope::name spelling unchanged. +# +# WHAT IS PINNED, and why each arm is a real contrast: +# (A) the map prints sc= rows at all (RED on the pre-change binary: zero rows — the presence guard). +# (B) the multiset of p::sc::n composed from the rows EQUALS the multiset of id= values the pre-change +# binary printed on the same fixture (embedded below, captured 2026-09-12 from main @ 1cf3086e). +# This is the lossless-ness proof: nothing a reader could address before is unaddressable now. +# (C) every composed id round-trips through --expand: every body served carries the composed path as +# p= and the composed name as n= (the resolver's own answer, an independent derivation), and a +# mutated scope serves nothing — so the scope segment is read, not skipped to a bare-name union. +# (D) the old id= spelling stays ACCEPTED on input (--expand and --callers), naming the same symbol. +# (E) the old shape is gone from the rows (no id= on / rows) and the legend defines sc= (the +# legend-coverage contract: every attribute a reader meets has a name= definition). +# (F) the lens rows (--for's ) compose the same way with the row's OWN p=. +# (G) the JSON twin carries "sc" and no "id" on the same rows (attribute-name parity, mcpattrparity). +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +cd "$ROOT" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN"; exit 2; } +TMP="$( mktemp -d "${TMPDIR:-/tmp}/scroundtrip.XXXXXX" )"; trap 'rm -rf "$TMP"' EXIT +echo "scroundtripcheck: BIN=$BIN" + +# The id= values the PRE-CHANGE binary printed on each fixture's flagless map (sorted, one per line, duplicates +# kept: a class row and its constructor row share one canonical id by construction). Captured from +# `ripwire test/` at main @ 1cf3086e, 2026-09-12 — the population arm (B) compares against. +expected_cppqualfix='qual.cpp::Base::Base +qual.cpp::Box::Box +qual.cpp::Box::get +qual.cpp::Derived::Derived +qual.cpp::Tag::Tag +qual.cpp::Widget::Widget +qual.cpp::Widget::Widget +qual.cpp::Widget::make +qual.cpp::Widget::ping +qual.cpp::a::scopedTmpl +qual.cpp::a::twoSeg +qual.cpp::b::mutexFn +qual.cpp::b::pick +qual.cpp::b::targetFn' +expected_nestedqualfix='defs.hpp::Outer::Inner +defs.hpp::Outer::Inner::Inner +defs.hpp::SOuter::SInner +defs.hpp::SOuter::SInner::SInner +outer.hpp::Base::Base +outer.hpp::Decoy::Decoy +outer.hpp::Inner::Inner +outer.hpp::Outer::Outer +outer.hpp::Outer::Outer +outer.hpp::SOuter::SOuter +outer.hpp::SOuter::SOuter' + +# compose p::sc::n from a map: every wrapper, then each row under it. Attribute ORDER on +# the row is not assumed (n= and sc= are found by name); a row without sc= composes nothing. +cat > "$TMP/compose.py" <<'PY' +import re, sys +doc = open( sys.argv[1], encoding = "utf-8", errors = "replace" ).read() +tag = sys.argv[2] if len( sys.argv ) > 2 else "s" +out = [] +if tag == "s": + for f in re.finditer( r']*>(.*?)', doc, re.S ): + p = f.group( 1 ) + for row in re.finditer( r']*)>', f.group( 2 ) ): + a = dict( re.findall( r'\s([\w:.-]+)="([^"]*)"', row.group( 1 ) ) ) + if "sc" in a: out.append( p + "::" + a[ "sc" ] + "::" + a[ "n" ] ) +else: + for row in re.finditer( r']*)>', doc ): + a = dict( re.findall( r'\s([\w:.-]+)="([^"]*)"', row.group( 1 ) ) ) + if "sc" in a and "p" in a: out.append( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ) +sys.stdout.write( "\n".join( sorted( out ) ) + ( "\n" if out else "" ) ) +PY + +for fx in cppqualfix nestedqualfix; do + "$BIN" "test/$fx" --no-cache >"$TMP/$fx.map" 2>/dev/null "$TMP/$fx.composed" + nsc="$( grep -c . "$TMP/$fx.composed" )" + # (A) presence: sc= rows exist at all + if [ "$nsc" -gt 0 ]; then ok "(A) $fx: $nsc scoped rows carry sc="; else no "(A) $fx: no row on the map (the short id is not emitted)"; fi + # (B) the composed multiset equals the pre-change id= multiset + eval "printf '%s\n' \"\$expected_$fx\"" | sort >"$TMP/$fx.expected" + if [ "$nsc" -gt 0 ] && cmp -s "$TMP/$fx.composed" "$TMP/$fx.expected"; then + ok "(B) $fx: p::sc::n composed from the rows == the id= multiset the pre-change binary printed ($nsc ids)" + else + no "(B) $fx: composed ids differ from the pre-change id= set"; diff "$TMP/$fx.expected" "$TMP/$fx.composed" | head -6 | sed 's/^/ /' + fi + # (C) round trip: --expand= resolves to the body of THAT symbol — every served carries the + # composed path as p= and the composed name as n= — and a scope that names nothing refuses (the contrast + # that proves the scope segment is read, not skipped past to a bare-name union). + bad=0; n=0 + while IFS= read -r cid; do + [ -n "$cid" ] || continue + n=$(( n + 1 )) + cpath="${cid%%::*}"; cname="${cid##*::}" + out="$( "$BIN" "test/$fx" --no-cache --top-k=0 "--expand=$cid" 2>&1 ]*>' | wc -l | tr -d ' ' )" + nbmatch="$( printf '%s' "$out" | grep -o ']*>' | grep -c " p=\"$cpath\"" )" + nbname="$( printf '%s' "$out" | grep -o ']*>' | grep -c " n=\"$cname\"" )" + if [ $rc -ne 0 ] || printf '%s' "$out" | grep -q 'symbol not found\|matched no symbol' \ + || [ "$nb" -lt 1 ] || [ "$nbmatch" != "$nb" ] || [ "$nbname" != "$nb" ]; then + bad=$(( bad + 1 )); [ $bad -le 2 ] && printf ' --expand=%s: rc=%s bodies=%s path-matched=%s name-matched=%s\n' "$cid" "$rc" "$nb" "$nbmatch" "$nbname" + fi + done < <( sort -u "$TMP/$fx.composed" ) + if [ "$n" -gt 0 ] && [ "$bad" -eq 0 ]; then ok "(C) $fx: all $n composed ids round-trip through --expand to bodies of that path and name" + else no "(C) $fx: $bad of $n composed ids did not round-trip"; fi + first="$( sort -u "$TMP/$fx.composed" | head -1 )" + if [ -n "$first" ]; then + wrong="${first%%::*}::ZZnoScope::${first##*::}" + out="$( "$BIN" "test/$fx" --no-cache --top-k=0 "--expand=$wrong" 2>&1 ]* id="' "$TMP/$fx.map"; then no "(E) $fx: an row still prints id= (the short id did not replace it)"; else ok "(E) $fx: no row prints id="; fi + if grep -o '' "$TMP/$fx.map" | grep -q 'sc='; then ok "(E) $fx: the legend defines sc="; else no "(E) $fx: the legend never spells sc="; fi +done + +# (D) the old id= spelling stays accepted on input — the same symbol's body / callers +OLDID='qual.cpp::Widget::ping' +out="$( "$BIN" test/cppqualfix --no-cache --top-k=0 "--expand=$OLDID" 2>&1 ]*>' | grep -q ' p="qual.cpp"[^>]* n="ping"'; then ok "(D) --expand still accepts the path::scope::name spelling ($OLDID)"; else no "(D) --expand refused the old id= spelling (rc=$rc)"; fi +out="$( "$BIN" test/cppqualfix --no-cache "--callers=$OLDID" 2>&1 "$TMP/for.xml" 2>/dev/null "$TMP/for.composed" +if grep -qxF 'qual.cpp::a::scopedTmpl' "$TMP/for.composed"; then ok "(F) --for's row composes p::sc::n = qual.cpp::a::scopedTmpl"; else no "(F) --for's row did not compose qual.cpp::a::scopedTmpl (got: $( tr '\n' ' ' <"$TMP/for.composed" | head -c 200 ))"; fi +if grep -q ']* id="' "$TMP/for.xml"; then no "(F) a row still prints id="; else ok "(F) no row prints id="; fi + +# (G) the JSON twin: "sc" present, "id" absent on the symbol rows +"$BIN" test/cppqualfix --no-cache --json >"$TMP/map.json" 2>/dev/null +# that had just printed the path, on a document carrying no legend at all — a repetition AND an undefined +# first-screen attribute on the one --expand shape with nothing else to read. +"$BIN" test/nestedqualfix --expand=Outer --no-cache >"$TMP/wf.xml" 2>/dev/null ]*id="outer.hpp::' "$TMP/wf.xml"; then + ok "(E2) whole-file --expand anchor rows carry sc= and no path-repeating id=" + else + no "(E2) whole-file --expand anchor rows still spell id= (or lost sc=): $( grep -o ']*/>' "$TMP/wf.xml" | head -1 )" + fi + # …and the composition is DEFINED there, which it never was: the row's p= comes from its . + grep -q 'composes as p::sc::n' "$TMP/wf.xml" \ + && ok "(E2) the whole-file root defines the composition (sc= is not an undefined first-screen attribute)" \ + || no "(E2) the whole-file serving carries no legend defining sc=/the composition" + # the composed id still RESOLVES, which is the whole contract + "$BIN" test/nestedqualfix --expand='outer.hpp::Outer::Outer' --no-cache >"$TMP/wf2.xml" 2>/dev/null wrapper, no p= repetition) — pinned so the design +# stays a decision rather than an omission nobody re-checked. +"$BIN" test/cppqualfix --for=widget --format=candidates --no-cache >"$TMP/cand.xml" 2>/dev/null ]*p="' "$TMP/cand.xml" \ + && ok "(E2) --format=candidates rows carry p= (the flat export names its own file)" \ + || no "(E2) --format=candidates rows carry no p=: $( grep -o ']*/>' "$TMP/cand.xml" | head -1 )" + +# (E3) THE SERVING MODE IS A ROOT ATTRIBUTE, AND THE LEGEND SELECTOR READS IT THERE (CodeRabbit, PR #215). +# --expand has two servings with two compact schemas, and applyCompactDialect chose between them by searching +# the WHOLE document for `mode="whole-file"`. An --expand BUNDLE carries its bodies as CDATA, so any body that +# merely MENTIONS that literal — a gate script grepping for it, a doc quoting it, this very file — took the +# expand-file legend onto a document made of //. A legend describing a shape the document +# does not contain is the defect the expand-file schema was ADDED to fix; this arm is the other direction. +E3="$TMP/e3"; mkdir -p "$E3" +{ printf 'def alpha():\n marker = %s\n return marker\n\n' "'mode=\"whole-file\"'" + i=0; while [ "$i" -lt 400 ]; do printf 'def pad%s():\n return %s\n\n' "$i" "$i"; i=$(( i + 1 )); done; } > "$E3/a.py" +"$BIN" "$E3" --expand=alpha --legend=compact --no-cache >"$TMP/e3.xml" 2>/dev/null "$TMP/e3b.xml" 2>/dev/null 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: 89.5% fewer bytes at top-10, 81.8% at top-50, 84.3% at top-100 (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.") +add(S3, f"{BIN} . --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.") add(S3, f"{BIN} . --outline=rankGraphTeleport --top-k=0", "Control-flow skeleton of one symbol, payload-only via the new --top-k=0.") add(S3, f"{BIN} . --outline=rankGraphTeleport:1-10 --top-k=0", "CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).") add(S3, f"{BIN} . --expand=rankGraphTeleport --top-k=0", "Full body + inline callee signatures.") diff --git a/test/skilltruthcheck.sh b/test/skilltruthcheck.sh index e6725ab70..790106995 100755 --- a/test/skilltruthcheck.sh +++ b/test/skilltruthcheck.sh @@ -130,7 +130,7 @@ grep -q -- '--lint .*cache-\* data-layout' <<<"$helpOut" \ && ok "--help's --lint line names the cache-* data-layout pack" \ || no "--help's --lint line does not name the cache-* pack" -pushBackRow="$( "$BIN" "$ROOT" --metrics --no-cache --top-k=5000 2>/dev/null | grep -o ']*n="push_back"[^>]*svector\.h::svector::push_back[^>]*>' | head -1 )" +pushBackRow="$( "$BIN" "$ROOT" --metrics --no-cache --top-k=5000 2>/dev/null | grep -o ']*n="push_back" sc="svector"[^>]*>' | head -1 )" { [ -n "$pushBackRow" ] && grep -q 'ev="2"' <<<"$pushBackRow" && grep -q 'ev_why="guard-return:1"' <<<"$pushBackRow"; } \ && ok "--metrics actually emits ev=/ev_why= on a known guard-return function (svector::push_back)" \ || no "--metrics did not emit the expected ev=/ev_why= on svector::push_back" @@ -174,4 +174,106 @@ helpOut2="$( "$BIN" --help=all 2>/dev/null )" && ok "--help still ships --slice=/--slice-flow (the flags the find-bug skill now names)" \ || no "--help no longer ships --slice=/--slice-flow — the skill fix now names a retired flag" +# A1-2 (owner decision 2026-09-12): every `ripwire

--VERB…` a skill spells carries --legend=compact +# WHERE THE BINARY ACCEPTS IT. --for is exempt by policy (its compact legend is its own and the first call of a +# session wants the full one); everything else is decided by the BINARY, not by a list kept here. +# +# WHY NOT A LIST (PR #215 review item 5). This arm used to hold SKILL_COMPACT_VERBS, sixty verb names typed out +# by hand, and it named `zoom`. skills/ripwire-orient/SKILL.md spells `ripwire --zoom --legend=compact +# --mermaid`, which the binary REFUSES ("--legend=compact applies to the XML verbs only — --mermaid has no XML +# legend to compact") — so the gate was enforcing a broken command, and would have kept enforcing it. The verb +# is not what decides; the whole command is, because --mermaid/--html/--situ/the writer flags turn an XML verb +# into a non-XML run. Two more broken lines in ripwire-quality-bar/SKILL.md fell out of the same probe, which is +# the argument for asking the binary: a hand list cannot find what nobody thought to type into it. +# +# THE PROBE. Each distinct command that carries --legend=compact is RUN against an empty temp directory. The +# refusal is a parse-time check, so an empty corpus answers it in milliseconds and a bogus SYM operand cannot +# mask it; nothing but the refusal line is read, and no other failure counts. +SKILL_TMP="$( mktemp -d )"; trap 'rm -rf "$SKILL_TMP"' EXIT +SKILL_EMPTY="$SKILL_TMP/empty" +mkdir -p "$SKILL_EMPTY" +BT='`' +sc_checked=0; sc_bad=0 +# every distinct `ripwire …--legend=compact…` command span the skills spell +grep -rhoE --include='*.md' -- "ripwire --[a-z0-9-]+[^$BT]*" "$ROOT/skills" \ + | grep -F -- '--legend=compact' | sed 's/[[:space:]]*$//' | sort -u > "$SKILL_TMP/skill_compact_cmds.txt" +while IFS= read -r cmd; do + [ -n "$cmd" ] || continue + args="${cmd#ripwire }" + sc_checked=$(( sc_checked + 1 )) + # shellcheck disable=SC2086 + if "$BIN" "$SKILL_EMPTY" $args >/dev/null 2>"$SKILL_TMP/skillprobe.err"; then :; fi + if grep -q 'applies to the XML verbs only' "$SKILL_TMP/skillprobe.err"; then + sc_bad=$(( sc_bad + 1 )) + [ "$sc_bad" -le 5 ] && printf ' REFUSED: %s\n' "$( printf '%s' "$cmd" | head -c 140 )" + fi +done < "$SKILL_TMP/skill_compact_cmds.txt" +[ "$sc_checked" -gt 0 ] || no "skills compact policy: no skill command carries --legend=compact — the arm inspected nothing" +[ "$sc_bad" -eq 0 ] \ + && ok "skills compact policy: all $sc_checked distinct --legend=compact commands are ACCEPTED by this binary (asked, not listed)" \ + || no "skills compact policy: $sc_bad of $sc_checked --legend=compact commands are REFUSED by this binary (listed above) — the flag does not belong on them" +# …and the other direction: --for must NOT carry it (the one policy exemption this gate does state, because it +# is a choice and not a refusal — the binary accepts --legend=compact on --for perfectly well). +sc_for=0 +while IFS= read -r line; do + [ -n "$line" ] || continue + sc_for=$(( sc_for + 1 )) +done <<<"$( grep -rhoE --include='*.md' -- "ripwire --for=[^$BT]*--legend=compact[^$BT]*" "$ROOT/skills" )" +[ "$sc_for" -eq 0 ] \ + && ok "skills compact policy: no --for command carries --legend=compact (its compact legend is its own)" \ + || no "skills compact policy: $sc_for --for command(s) carry --legend=compact" + +# A1-2b (PR #215 review 5192692319): …AND THE OMISSION, which is the direction the two arms above cannot +# see. Both of them start from a command that ALREADY carries --legend=compact, so a command that should +# carry it and does not was invisible to every arm in this gate: skills/ripwire-fresh-eyes/SKILL.md's +# pass 1a spelled `ripwire --rank-by=churn-decay` with no flag from #218 until this arm existed, and +# five more spans across four other skills fell out of the same sweep. A policy gate that only audits the +# commands that obey it is not auditing the policy. +# +# THE PREDICATE, both halves asked of the BINARY and not of a list (the A1-2 argument, unchanged). A +# flagless span is REQUIRED to carry the flag when (a) the bare command EMITS XML on the probe corpus — +# stdout's first byte is '<' — and (b) appending --legend=compact is not refused. +# (a) IS WHAT KEEPS THE ARM SOUND, and it is not decoration: a span carrying a placeholder operand +# (`--arch=rules.txt`, `--scip=index.scip`, `--export=cc.json[:FILE]`) or a writer/non-XML flag fails +# before the legend check is ever reached, so its silence on the refusal line would otherwise read as +# "the flag belongs here". Measured on the pre-fix tree: 21 spans looked like violations without (a) and +# 6 were real. +# FLOOR, stated because silence here would read as a guarantee: (a) excludes every span whose operand +# cannot resolve against an empty corpus, so this arm is a FLOOR on the policy, never a total. The spans +# it skips are unproven in both directions — not proven exempt. +sc_classify(){ # 0 = must carry the flag and does not; 1 = not required (skip/exempt) + local args="$1" + # shellcheck disable=SC2086 + "$BIN" "$SKILL_EMPTY" $args >"$SKILL_TMP/bare.out" 2>/dev/null || true + [ "$( head -c 1 "$SKILL_TMP/bare.out" 2>/dev/null )" = '<' ] || return 1 # (a) not an XML run here + # shellcheck disable=SC2086 + "$BIN" "$SKILL_EMPTY" $args --legend=compact >/dev/null 2>"$SKILL_TMP/omit.err" || true + grep -q 'applies to the XML verbs only' "$SKILL_TMP/omit.err" && return 1 # (b) the binary refuses it + return 0 +} +# POSITIVE CONTROL FIRST, so a green below is never the classifier quietly failing every span. `--flags` +# is an XML verb the binary accepts the flag on, so a flagless `--flags` MUST classify as a violation. +if sc_classify "--flags"; then + ok "skills compact policy (omission) control: the classifier marks a flagless XML verb (--flags) as a violation — it can go red" +else + no "skills compact policy (omission) control: a flagless --flags did NOT classify as a violation — the classifier is inert and the sweep below proves nothing" +fi +sc_swept=0; sc_xml=0; sc_missing=0 +grep -rhoE --include='*.md' -- "ripwire --[a-z0-9-]+[^$BT]*" "$ROOT/skills" \ + | sed 's/[[:space:]]*$//' | sort -u \ + | grep -v -F -- '--legend=compact' | grep -vE '^ripwire --for' > "$SKILL_TMP/skill_flagless.txt" +while IFS= read -r cmd; do + [ -n "$cmd" ] || continue + sc_swept=$(( sc_swept + 1 )) + args="${cmd#ripwire }" + if sc_classify "$args"; then + sc_xml=$(( sc_xml + 1 )); sc_missing=$(( sc_missing + 1 )) + [ "$sc_missing" -le 6 ] && printf ' MISSING --legend=compact: %s\n' "$( printf '%s' "$cmd" | head -c 140 )" + fi +done < "$SKILL_TMP/skill_flagless.txt" +[ "$sc_swept" -gt 0 ] || no "skills compact policy (omission): no flagless skill command was found at all — the sweep inspected nothing" +[ "$sc_missing" -eq 0 ] \ + && ok "skills compact policy (omission): $sc_swept flagless span(s) swept, none both emits XML and accepts --legend=compact (floor: placeholder operands are unprovable on an empty corpus)" \ + || no "skills compact policy (omission): $sc_missing of $sc_swept flagless span(s) emit XML and accept --legend=compact but do not carry it (listed above) — the policy requires it" + [ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } diff --git a/test/stdqualcheck.sh b/test/stdqualcheck.sh index 8cc02ec8f..c8fce3a6e 100755 --- a/test/stdqualcheck.sh +++ b/test/stdqualcheck.sh @@ -138,7 +138,7 @@ printf '%s' "$MAP" | grep -qE 'n="launderIt"[^>]*amb=' \ # Measured on the pre-fix binary: zero callees — the brace-initialised temporary names no call reference that # reaches operator(). Pinned at that literal so the guard is seen to invent nothing; the vacuity guard asserts # the specialization's operator() really is indexed (so the zero is about resolution, not a missing file). -printf '%s' "$MAP" | grep -qE ' specialization's operator() IS indexed (scope hash)" \ || no "the specialization's operator() is missing from the map — the arm below is vacuous" expect callees hashMine 0 "std::hash{}( m ) — no edge today, none after: pinned, not invented" @@ -346,8 +346,8 @@ nrun --pin-census="$TMP/n.tsv" >"$TMP/nmap.xml" NMAP="$( cat "$TMP/nmap.xml" )" [ -s "$TMP/n.tsv" ] || no "§11: the nested-fixture census run wrote nothing — every census arm below would be vacuous" nmissing="" -for want in 'id="decoys.h::Pool::move"' 'id="decoys.h::chrono::duration_cast"' 'id="compat.h::std::terminate"' \ - 'id="polyfill.h::ranges::contains"' 'n="shiftRange"' 'n="toMillis"' 'n="bail"' 'n="drain"' 'n="sampleTicks"' 'n="hasAnswer"'; do +for want in 'n="move" sc="Pool"' 'n="duration_cast" sc="chrono"' 'n="terminate" sc="std"' \ + 'n="contains" sc="ranges"' 'n="shiftRange"' 'n="toMillis"' 'n="bail"' 'n="drain"' 'n="sampleTicks"' 'n="hasAnswer"'; do printf '%s' "$NMAP" | grep -qF "$want" || nmissing="$nmissing $want" done [ -z "$nmissing" ] \ diff --git a/test/taskechocheck.sh b/test/taskechocheck.sh index 39a4cedf7..c66d696b1 100755 --- a/test/taskechocheck.sh +++ b/test/taskechocheck.sh @@ -99,11 +99,14 @@ if [ "$jpt" = "$xpt" ]; then ok "--pack-task: the two dialects report the SAME # ── §B1.7 arm 3: the route note is recoverable verbatim too ──────────────────────────────────────── rfor="$( printf '%s' "$XMLFOR" | rootAttr route )" +# row 6 (2026-09-12): route= is a CODE (name-exact(X) / subtoken+body[:broad|:declined(...)]), so the value the +# router produced is recoverable verbatim when it starts with one of the two codes — the dash-collapse defect +# this arm was written for cannot recur on a value that never held a flag name, and the arm now asserts the +# code itself rather than a flag spelling the value no longer carries. case "$rfor" in - "@@MISSING@@") no "--for XML root has no route= attribute";; - *"--for"*) ok "--for XML root route= keeps its double-hyphen flag names verbatim";; - *"-for"*) no "--for XML root route= is still dash-collapsed ('$rfor')";; - *) no "--for XML root route= names no ranker flag at all ('$rfor')";; + "@@MISSING@@") no "--for XML root has no route= attribute";; + "name-exact("*|"subtoken+body"*) ok "--for XML root route= carries the ranker code verbatim ('$rfor')";; + *) no "--for XML root route= is not a ranker code ('$rfor')";; esac # ── §B1.7 arm 4: the SCRUB itself is untouched — the comment echo is still collapsed and G4-legal ── @@ -229,10 +232,10 @@ if command -v xmllint >/dev/null 2>&1; then # the scrub is LOSSY BY DESIGN and the note says which bytes it eats: a control byte becomes a space and # an invalid sequence becomes '?'. Asserting the substitution (not just well-formedness) is what stops a # future "fix" from silently dropping the byte and shortening the user's own token. - grep -q 'names a symbol (ceilingArithmetic x)' "$TMP/route.c0.xml" \ + grep -q 'name-exact(ceilingArithmetic x)' "$TMP/route.c0.xml" \ && ok "§B4 the C0 byte became a SPACE inside the comment (xmlCommentText rule 2)" \ || no "§B4 the C0 byte was not replaced by a space: [$( head -c 90 "$TMP/route.c0.xml" )]" - grep -q 'names a symbol (ceilingArithmetic?x)' "$TMP/route.utf8.xml" \ + grep -q 'name-exact(ceilingArithmetic?x)' "$TMP/route.utf8.xml" \ && ok "§B4 the invalid UTF-8 byte became '?' inside the comment (xmlCommentText rule 3)" \ || no "§B4 the invalid UTF-8 byte was not replaced by '?': [$( head -c 90 "$TMP/route.utf8.xml" )]" else diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 4572832ee..fc4489b84 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -281,7 +281,7 @@ OR="$( route 'clang says the inner loop was not vectorized - is that worth a dif case "$OR" in *'status="recommend"'*'intent="opt-remark"'*'skill="ripwire-opt-remarks"'*'--for='*) ok "a clang optimization remark -> the ranked lens, under the opt-remarks skill";; *) no "opt-remark route wrong: $OR";; esac # Execution check: the two catalog commands that carry a COMPOSED value are not placeholders. Unquote # what the router emitted and run it through the real verb, the same way the SYM:VAR arm above does. -GQEXPR="$( printf '%s' "$GQ" | sed -n 's|.*--graph-query='\(.*\)'.*|\1|p' | sed 's/"/"/g' )" +GQEXPR="$( printf '%s' "$GQ" | sed -n 's|.*--graph-query='\(.*\)'\( --legend=compact\)\{0,1\}.*|\1|p' | sed 's/"/"/g' )" GQRUN="$( "$BIN" "$REPO" --no-cache --graph-query="$GQEXPR" )"; rc=$? { [ $rc -eq 0 ] && printf '%s' "$GQRUN" | grep -q ' the router emits over the whole prompt +# corpus is executed against an EMPTY directory, and the refusal line must never appear. The refusal is a +# parse-time check, so an empty corpus answers in milliseconds and no operand can mask it. +RLEG_EMPTY="$TMP/rleg_empty"; mkdir -p "$RLEG_EMPTY" +cut -f5 "$ROOT/test/taskroutefix/prompts.tsv" 2>/dev/null | sed '1d' > "$TMP/rleg_tasks.txt" # column 5 is the prompt +: > "$TMP/rleg_cmds.txt" +while IFS= read -r _t; do + [ -n "$_t" ] || continue + route "$_t" 2>/dev/null | grep -oE '[^<]*' | sed 's/^//' >> "$TMP/rleg_cmds.txt" +done < "$TMP/rleg_tasks.txt" +# the line is XML-escaped; unescape the two entities the router can emit, then drop the leading `ripwire` +sed -e "s/'/'/g" -e 's/"/"/g' -e 's/&/\&/g' "$TMP/rleg_cmds.txt" | sort -u > "$TMP/rleg_u.txt" +# THE STATUS, CAPTURED RATHER THAN SWALLOWED (CodeRabbit, PR #215). This ran each command under +# `eval … || true`, which discards every exit status unconditionally: the arm could not tell a clean run from +# a crash, and the only failure it could see was the one refusal string it greps for. Deleting the `|| true` +# is not the fix either — most of these routes point at an EMPTY directory, where a nonzero exit is the +# DOCUMENTED answer, not a defect. So each status is captured and compared against what a routed command may +# legitimately return here: +# 0 the verb answered; +# 1 the verb's own documented "nothing in this corpus to answer" degrade — no symbol matched, no plan file +# to read, no ranked candidate; and for the --test-gate route, "no files given and no git diff". +# MEASURED over the whole corpus on this binary (2026-09-14): 34 distinct commands, 15 exit 0 and 19 exit 1, +# nothing else. Any OTHER status is reported: 2 is cannot-conclude, 3 a token-budget refusal, 4 a test-gate +# blast radius, >=128 a signal — each one means the router emitted a command that neither answered nor +# degraded, and `|| true` printed PASS for every one of them. +# The stderr side widens for the same reason: the compact-legend refusal was the only parse refusal looked +# for, so its siblings — an unknown flag, an unrecognised verb — were invisible. A command the parser refuses +# is a wrong command whatever the wording, and a parse refusal is reported even when the status looks benign. +rleg_n=0; rleg_bad=0; rleg_st_bad=0; rleg_st0=0; rleg_st1=0 +while IFS= read -r _c; do + [ -n "$_c" ] || continue + _args="${_c#ripwire }" + _args="${_args#\'*\' }" # the quoted root the router spells; this arm supplies its own + rleg_n=$(( rleg_n + 1 )) + # shellcheck disable=SC2086 + eval "\"\$BIN\" \"\$RLEG_EMPTY\" $_args" >/dev/null 2>"$TMP/rleg.err" + _st=$? + case "$_st" in + 0) rleg_st0=$(( rleg_st0 + 1 )) ;; + 1) rleg_st1=$(( rleg_st1 + 1 )) ;; + *) rleg_st_bad=$(( rleg_st_bad + 1 )) + if [ "$rleg_st_bad" -le 5 ]; then + printf ' EXIT %s (expected 0 or the documented 1): %s\n' "$_st" "$( printf '%s' "$_c" | head -c 140 )" + printf ' stderr: %s\n' "$( head -c 160 "$TMP/rleg.err" | tr '\n' ' ' )" + fi ;; + esac + if grep -qE 'applies to the XML verbs only|unknown flag|unknown verb|unrecognized' "$TMP/rleg.err"; then + rleg_bad=$(( rleg_bad + 1 )) + [ "$rleg_bad" -le 5 ] && printf ' REFUSED (exit %s): %s\n' "$_st" "$( printf '%s' "$_c" | head -c 140 )" + fi +done < "$TMP/rleg_u.txt" +[ "$rleg_n" -gt 0 ] || no "R-LEG: the prompt corpus produced no command — this arm proved nothing" +[ "$rleg_bad" -eq 0 ] \ + && ok "R-LEG: all $rleg_n distinct generated commands are ACCEPTED by this binary (the router cannot emit a command its own binary refuses)" \ + || no "R-LEG: $rleg_bad of $rleg_n generated commands are REFUSED by this binary (listed above)" +[ "$rleg_st_bad" -eq 0 ] \ + && ok "R-LEG: every generated command exited 0 (answered: $rleg_st0) or 1 (documented empty-corpus degrade: $rleg_st1) — no unexpected status, none swallowed" \ + || no "R-LEG: $rleg_st_bad of $rleg_n generated commands exited with an unexpected status (listed above) — the old '|| true' reported PASS for these" + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" diff --git a/test/usesselectorcheck.sh b/test/usesselectorcheck.sh index 5b32632d9..6ba2cb38d 100755 --- a/test/usesselectorcheck.sh +++ b/test/usesselectorcheck.sh @@ -122,7 +122,20 @@ grep -q 'srcmut_sigchange' "$TMP/err" && no "the constant nonsense suggestion (s # Bundle mode's body tag carries no id= at all, so --expand is no longer a mode-independent way to # fetch a canonical id; --outline always rides the classic 200-row map (no V1 lean default applies to # it) and its rows carry id= unconditionally, so it is the stable lookup path here. -CANON_ID="$( "$BIN" "$ROOT" --outline='src/notes.h:empty' --no-cache 2>/dev/null | grep -o 'id="[^"]*NoteIndex::empty"' | head -1 | sed 's/^id="//;s/"$//' )" +# row 6 (2026-09-12): the row prints the short id sc=; the canonical id composes as ::sc::n +CANON_ID="$( "$BIN" "$ROOT" --outline='src/notes.h:empty' --no-cache 2>/dev/null | python3 -c ' +import re, sys +doc, name, scope = sys.stdin.read(), sys.argv[1], ( sys.argv[2] if len( sys.argv ) > 2 else None ) +for f in re.finditer( r"]*>(.*?)", doc, re.S ): + for row in re.finditer( r"<[sd]\b([^>]*)>", f.group( 2 ) ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and ( scope is None or a[ "sc" ] == scope ): + print( f.group( 1 ) + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +for row in re.finditer( r"]*)>", doc ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if a.get( "n" ) == name and "sc" in a and "p" in a and ( scope is None or a[ "sc" ] == scope ): + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); sys.exit( 0 ) +' empty NoteIndex )" [ -n "$CANON_ID" ] || { no "could not look up NoteIndex::empty's canonical id via --outline"; CANON_ID="./src/notes.h::NoteIndex::empty"; } BARE_A="$( uses_elem 'buildGraph' )" BARE_B="$( uses_elem 'buildGraph' )" diff --git a/test/wrapverbscheck.sh b/test/wrapverbscheck.sh index b40dffdd5..903b595b4 100755 --- a/test/wrapverbscheck.sh +++ b/test/wrapverbscheck.sh @@ -314,6 +314,63 @@ for _agent in claude opencode aider; do done # ─── Summary ────────────────────────────────────────────────────────────────── +echo +echo "=== 7. agent surfaces ask for the compact legend (A1-2, owner decision 2026-09-12) ===" +# Every command the blurb spells for an XML verb carries --legend=compact (the legend is most of a small +# --callers/--uses/--impact answer, and byte-identical rows either way); --for never does (its compact legend is its +# own, and the first call of a session wants the full one). The verb list is the shipped policy, spelled here so a +# blurb edit that drops the flag on one of them is red, not a judgement call. +COMPACT_VERBS="callers impact uses expand exemplar quality-delta test-gate pack-task from-trace edit-check" +BT='`' +"$BIN" wrap claude 2>/dev/null | blurb_body >"$TMP/blurb7" +[ -s "$TMP/blurb7" ] || no "7 presence: no blurb body to inspect" +for _v in $COMPACT_VERBS; do + _spans="$( grep -oE -- '`[^`]*`' "$TMP/blurb7" | grep -E -- "--$_v(=|$BT| )" )" + [ -n "$_spans" ] || { no "7 presence: the blurb no longer spells --$_v — re-author this arm"; continue; } + if printf '%s\n' "$_spans" | grep -vq -- '--legend=compact'; then + no "7: a blurb command for --$_v lacks --legend=compact: $( printf '%s\n' "$_spans" | grep -v -- '--legend=compact' | head -1 )" + else + ok "7: every blurb command for --$_v carries --legend=compact" + fi +done +if grep -oE -- '`[^`]*`' "$TMP/blurb7" | grep -E -- '--for=' | grep -q -- '--legend=compact'; then + no "7: a --for command in the blurb carries --legend=compact (the first call wants the full legend; --for's compact legend is its own)" +else + ok "7: no --for command in the blurb carries --legend=compact" +fi + +echo +echo "=== 8. every agent surface also says how to get the FULL legend back (owner question, 2026-09-13) ===" +# Compact is what the generated commands ask for; an agent must also know when and how to ask for the full +# legend (a term it does not recognise, a floor or cap it needs explained, a map a human will read). Four +# surfaces, one sentence each — the wrap blurb (the session-start primer extracts it), the router skill (the +# skills' shared conventions, not seventeen bodies), the prompt router's injected context, and the MCP +# schema's `legend` field. The phrase asserted is the one the four surfaces share; a surface that drops it +# is red, not a judgement call. +FULL_PHRASE='--legend=full' +if grep -qF -- "$FULL_PHRASE" "$TMP/blurb7" && grep -qiF 'definition' "$TMP/blurb7"; then + ok "8: the wrap blurb says when to add --legend=full" +else + no "8: the wrap blurb never says how to get the full legend back (no --legend=full line)" +fi +if grep -qF -- "$FULL_PHRASE" "$ROOT/skills/ripwire-router/SKILL.md"; then + ok "8: skills/ripwire-router/SKILL.md carries the --legend=full convention" +else + no "8: skills/ripwire-router/SKILL.md never mentions --legend=full" +fi +for _h in ripwire-claude-route.sh ripwire-claude-toolroute.sh ripwire-codex-route.sh; do + if grep -q -- "add --legend=full" "$ROOT/hooks/$_h"; then + ok "8: hooks/$_h's injected context says to add --legend=full when a definition is unclear" + else + no "8: hooks/$_h's injected context never mentions --legend=full" + fi +done +if printf '{"jsonrpc":"2.0","id":1,"method":"tools/list"}\n' | "$BIN" --mcp 2>/dev/null | grep -q 'restores the full legend'; then + ok "8: the MCP schema's legend field says \"full\" restores the full legend" +else + no "8: the MCP schema's legend field does not say that \"full\" restores the full legend" +fi + echo if [ "$fail" -eq 0 ]; then echo "ALL PASS"