From a2c43de95163f082b0738c802d263d2fc7ee9fc3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sat, 12 Sep 2026 22:11:16 -0400 Subject: [PATCH 01/22] perf(output): every scoped symbol row repeated its file path inside id=; it now prints the scope alone as sc= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE DEFECT. A scoped map row printed id="path::scope::name" under an wrapper that had just printed the same path, and a lens row printed it beside its own p=. On this repository's flagless map that was 137 of 137 scoped rows — 11.2% of the document on the 2026-09-12 re-measure. route= carried 107 B of prose ("routed: subtoken+body BM25 (--for's default) — no strong name hit, multi-word conceptual query") on every --for answer, and a compact bundle printed one row per same-named callee. THE FIX. The row carries sc= (the enclosing scope), the one segment nothing else on the page holds; the legend states the composition (the full id is p::sc::n, p= from the row or its ), and every selector keeps accepting the composed path::scope::name — the resolver was never changed. route= is a code (name-exact(X) | subtoken+body[:broad|:declined(word;carriers,defs)]) whose reading lives in the legend; the anchors: evidence clause is unchanged. Same-named callees of one calls block merge into ; shown= still counts callees and the legend says so. The sc=/route= reading is ONE shared clause (graphlegend.h kForIdRouteLegend) on the CLI lens and the MCP twin, ceiling-droppable with the confidence clause (the dropped note names it) and exempt from the signature-trim charge on both surfaces — charged, it dropped one ranked row on the MCP twin (mcpforparitycheck (2), the 2026-09-04 regression shape) and re-trimmed the explicit-ceiling sigs below the default's (forbudgetmonotoncheck #1/#5). The --json twins print "sc". MEASURED (wc -c, this commit's build against the pre-change build of the same tree): flagless map, this repository 26,402 -> 22,354 B (-4,048 B, -15.3%; the same 185 rows) test/cppqualfix 2,935 -> 2,781 B (-5.2%) test/nestedqualfix 2,045 -> 1,937 B (-5.3%) test/accessshapefix 1,953 -> 1,962 B (+9 B: four scoped rows do not pay for the longer reading) --for, three tasks on this tree: the bundle is byte-shaped, so the row savings served rows, not bytes — 25 -> 28, 21 -> 24 and 3 -> 3 signature rows at 10,042 -> 10,145, 10,256 -> 10,316, 5,971 -> 6,022 B; route= 111 -> 23 B per answer; the reading costs 259 B per default-dialect answer. MCP for, the same three tasks: 8,752 -> 9,056, 8,882 -> 9,149, 1,965 -> 2,173 B with 26 -> 30, 23 -> 27, 1 -> 1 rows. GATE, RED FIRST. test/scroundtripcheck.sh (new, listed in test/regression.sh; the gate count is regenerated to 613): against the pre-change binary 13 of 17 arms FAIL (no sc= row, the composed set is empty, no composes, --json carries "id"); against this build ALL PASS — the multiset of p::sc::n composed from the rows equals the id= multiset the old binary printed on cppqualfix (14) and nestedqualfix (11), every composed id resolves through --expand to bodies of that path and name, a mutated scope (ZZnoScope) serves no body, the old spelling still resolves on --expand and --callers, and --json mirrors the attribute. PINS MOVED, with the measured number: compactlegendcheck map 810 -> 920 (908), map-diff 800 -> 910 (901), pack-signatures 680 -> 780 (775), metrics 720 -> 820 (814), query 630 -> 730 (723), pack-task 820 -> 980 (974), pack-top-n 660 -> 770 (761) — the one new whole-document sc= reading; the ten-verb loop 4,900 -> 5,000 (4,946); fillordercheck est_tokens 884 -> 894; printf_parity.manifest re-pinned for exactly {flagless, expand, around, pack_signatures, pack_task, help_all}; five goldens regenerated (test/golden.xml, anchorfix/golden_for.xml, routefix/golden_for.xml, docdemotegolden_for.xml, docdemotegolden_noroute.xml — the row shape and the reading, nothing else in the diff); the showcase caption re-derived by its own recount (89.5/81.8/84.3 -> 91.8/85.2/85.5, both copies). mcpforparitycheck (6) re-authored onto an identifier query: "parse tree" routes subtoken+body:broad, the same ranker no_route forces, so the served sets could only differ by what the route prose displaced from the byte-shaped sigs — an artefact, not the argument. Thirty-one gates that asserted the id= shape now assert n=/sc= or compose the id; every honesty assertion is kept. Three legendcoverage baseline lines closed (for/for-auto/for-budgeted ctx@route); quality-delta gating=0 after acking this lane's own short-horizon churn; ASan clean on every touched emit path; determinism and xmllint hold on the map, --for (both dialects) and --pack-task. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 36 +++-- CHANGELOG.md | 33 ++++ README.md | 4 +- docs/COMMANDS.md | 4 +- docs/EVALS.md | 6 +- docs/captures/COMMANDS_showcase_2026-09-12.md | 2 +- present/deck5_ripwire_build.js | 6 +- src/cli.h | 2 +- src/compactlegend.h | 12 +- src/graphlegend.h | 16 ++ src/lexical.h | 13 +- src/mcpverbs.h | 12 +- src/packtask.h | 2 +- src/serialize.h | 143 ++++++++++------- src/tracelocus.h | 2 +- src/verbs_for.h | 32 +++- test/adaptivecheck.sh | 2 +- test/anchorfix/golden_for.xml | 2 +- test/bundleidcheck.sh | 16 +- test/candheadcheck.sh | 2 +- test/canoncheck.sh | 14 +- test/chainidcheck.sh | 17 +- test/childwalkscalecheck.sh | 2 +- test/compactlegendcheck.sh | 31 ++-- test/cppqualcheck.sh | 4 +- test/defoverdeclcheck.sh | 6 +- test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- test/duprowcheck.sh | 4 +- test/elixirnamearitycheck.sh | 13 +- test/fieldnarrowcheck.sh | 7 +- test/fillordercheck.sh | 5 +- test/golden.xml | 2 +- test/kotlincheck.sh | 12 +- test/legendcoverage_baseline.txt | 3 - test/mcpforparitycheck.sh | 6 +- test/nestedqualcheck.sh | 2 +- test/notescheck.sh | 15 +- test/packtaskcheck.sh | 15 +- test/packtaskquotacheck.sh | 15 +- test/printf_parity.manifest | 12 +- test/qualitykeycheck.sh | 17 +- test/regression.sh | 2 +- test/routecheck.sh | 34 ++-- test/routefix/golden_for.xml | 2 +- test/routeoncecheck.sh | 20 +-- test/rubyscopecheck.sh | 17 +- test/rubysettercheck.sh | 14 +- test/scroundtripcheck.sh | 146 ++++++++++++++++++ test/showcase_capture.py | 2 +- test/stdqualcheck.sh | 6 +- test/taskechocheck.sh | 15 +- test/usesselectorcheck.sh | 15 +- 53 files changed, 600 insertions(+), 226 deletions(-) create mode 100755 test/scroundtripcheck.sh diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 259dda583..d460f1320 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -728,6 +728,7 @@ ack short-horizon-churn 060a064b6ffa7775 44 W1-S2 churn-keying fix (pathQualifie ack short-horizon-churn 0749c4e602daa603 9 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 0777f290bdc69b11 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 07cf5773893d89b0 4 cid=d4a11b38ea395398 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. +ack short-horizon-churn 080eb5a5ef9c424d 2 cid=74766622551d0034 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 083de06af84b3a87 30 cid=82bfd94f1c162607 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 0a3d16d6f3139408 13 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack short-horizon-churn 0ab42b0dea75e0c1 4 cid=75fd6d9c66adca24 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) @@ -748,7 +749,7 @@ ack short-horizon-churn 129d3c8d5a763870 7 E1 answer grader + questions task sou ack short-horizon-churn 131068a6cedf0864 10 cid=bef0a5079a2bf8e4 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit ack short-horizon-churn 15061a69cb5b451f 4 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn 1520fa02411735c3 4 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 15754e3561a34f40 39 cid=93c7392c7b677557 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. +ack short-horizon-churn 15754e3561a34f40 79 cid=d17db54e2e6c277d row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack short-horizon-churn 1610c5acaa7d4806 6 cid=545658032a875c30 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 1624b02e9104560e 154 cid=85e3075128763497 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 163c0a0eb3219fa9 10 cid=9e7d5dab8c14a887 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. @@ -766,7 +767,7 @@ ack short-horizon-churn 1b4698af2f132a6c 8 cid=94f1d898d24191f6 arise-h2h lane 2 ack short-horizon-churn 1b4fa16bec10f38a 4 cid=a66263709c703a7d wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file ack short-horizon-churn 1bb815714e8daf12 13 cid=4966444e47d72201 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 1bded1f8f88d3b85 22 cid=3667b9c21e5f7382 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. | prior: arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked -ack short-horizon-churn 1c11c9480374c3a4 88 cid=27984fbee9fe12a9 by=src/* lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). +ack short-horizon-churn 1c11c9480374c3a4 118 cid=f15cb316253cdb8e by=src/* row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack short-horizon-churn 1cb330d8aa2e7320 3 cid=4b5bb633d5c8a487 abstention round-2 lane: short-horizon churn on the ARB harness is the lane's own second edit to bench/arb this wave — run_arb.py had to learn the three new facts and sweep() had to write them; the churn is the round, not new debt in the tool ack short-horizon-churn 1cdbcf525f9e7871 5 cid=4a92ba4004e888e5 lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack short-horizon-churn 1da01868deceb731 7 cid=add9cf9f36a5bdf8 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -780,6 +781,7 @@ ack short-horizon-churn 223d4dba0a2cf7ae 2 cid=112cf65f03ff55a8 by=src/* lane/n2 ack short-horizon-churn 229bca420785240c 5 cid=c7ac6bde7d98b8c7 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 232cb10582c81dcc 11 verb-ux R1/R2 lane 2026-08-12: churn=self rows are this lane's own commits (any edit here churns); the two verbosity rows are the in-band legend + assumed-root disclosure prose the items require ack short-horizon-churn 240fa4dc89f6442d 20 cid=ca5968a9693b8433 churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. +ack short-horizon-churn 24637f7c69207cb9 155 cid=f2f3046ea361eb5f row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 2467c48e0842da91 13 cid=9b69f827943239f7 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 25a63a660bcadb93 30 cid=654e5ffb88c52a1a answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 264ba131aa776d8a 24 cid=2af7af1d73041fce 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself @@ -796,6 +798,7 @@ ack short-horizon-churn 2a65a1bdce49a905 5 cid=04691f1666686e37 OPTREMARKS F3 (d ack short-horizon-churn 2a81cf1c5f079446 5 cid=62cbf3ccb5f7a6da OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 2abd26fdd3d1e6df 2 cid=80501bdf3db6fba6 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. ack short-horizon-churn 2b1f63de0a7dddc7 4 cid=cf11d0a32c58b8b2 wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file +ack short-horizon-churn 2dcfe59fcc0a6890 2 cid=0ca691bbfdbd1b10 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 2e6026bd58111ad5 6 cid=3d6b010d178e2c8f by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn 2ec57038731f8f2b 8 cid=7e52c807eb5fa5c3 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 2ee6d772911fc3ce 7 cid=88266d21dc326aad OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -843,7 +846,7 @@ ack short-horizon-churn 42ab5ed42c8d5187 5 cid=97f7b7bb44d49e47 OPTREMARKS F3 (d ack short-horizon-churn 431657fbc058d19b 5 cid=67f51a274892e20b OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 43248b413bc6fa84 13 cid=065138dfaceee3ad by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn 43e576a9c6592de5 3 cid=0e72cae094010b70 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself -ack short-horizon-churn 451442e5d1031096 39 cid=404e4166b03a1fec P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh +ack short-horizon-churn 451442e5d1031096 79 cid=4ac9561ab732e3fc row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh ack short-horizon-churn 458a97936164903a 13 cid=040d8874f9df08e8 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 45b52ada32f63c11 6 cid=8bd35141997faf18 M12 follow-up (capture-audit L9): --ensemble gained root=/root-relative p= — writeEnsembleReport's 3 new default-valued params (singleRoot/rootPrefix/rootAttr, back-compat) thread the caller's already-computed single-root spelling through; short-horizon-churn on the touched dispatcher. | prior: at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack short-horizon-churn 45bec7fbb1357cd7 71 cid=e622b64dd13745fb by=src/* §N6-C .gitignore-by-default: the crawl gains an ignore mode. The two api-surface/params rows are ONE deliberate contract change — ingest()/collectSources() take a trailing defaulted respectGitignore, the only way a CLI flag can reach the crawl without a global; the three short-horizon-churn rows are this lane's own edits to the flag ledger, the crawl and the --skipped verb, which is what adding a flag with a disclosure IS; collectSources +3 ccx / +11 LOC is what remains after the probe, the mode and the prune fan-out were extracted into probeIgnoreSet/recordDirPrune (it was +15/+43 inline). | prior: 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. @@ -887,16 +890,17 @@ ack short-horizon-churn 572dbeee3b452cbc 21 cid=e0a24e18a683a9b1 by=src/* M12 (c ack short-horizon-churn 5774f0f445361430 8 graphrag-recon idea #1 corroboration-weighted callers: renderD1CallerRows' own short-horizon-churn is the direct, expected, unavoidable cost of implementing the feature in its territory (src/packtask.h caller-section renderer) -- not a code-shape regression ack short-horizon-churn 578fa051307418ee 5 cid=307a821a72c35f00 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 59f050855874875f 14 cid=dbe0701cc173e389 by=src/* R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. -ack short-horizon-churn 5b224c7fe142bd56 37 cid=3352105024f32829 by=src/* lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: issue #61 disclosure lane: the only gating row is short-horizon-churn on runForLens (churn=self) — the --for XML emitter is where the over_ceiling verdict has to be computed, so editing it again this week is the fix, not thrash. The predicate itself was extracted OUT of that body into the file-scope forLensOverCeiling beside its JSON twin, which is why the verbosity row dropped to sev=minor (+6 LOC, all of it the call and its pointer comment) instead of carrying the whole METHODOLOGY §9 argument inline. Gate: test/formaxtokenscheck.sh, written red-first. +ack short-horizon-churn 5b224c7fe142bd56 52 cid=894118d22b83c03f by=src/* row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack short-horizon-churn 5c2c4a2b311b8dba 5 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 5d8f5288f0df10f7 95 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact ack short-horizon-churn 5e16b9596d9bd135 45 cid=743b6f0afb91f3cc by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack short-horizon-churn 5e5cc30bcbc1fb63 5 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn 5f6df3644e09d272 2 cid=b5b7f971f1b9428c by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 5f818eb4d34125e4 2 cid=adbc585f65c9fa38 L10: --doctor legend + blobs_floor= disambiguation (finding 6) — DoctorCacheStats gains capHit, doctorCacheStats sets it, runDoctor emits blobs_floor= and the new legend comment; short-horizon-churn and the verbosity bump are the direct, deliberate cost of that -ack short-horizon-churn 6031be13b40a1b6f 37 cid=e77e1519a7e2bd9d lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites +ack short-horizon-churn 6031be13b40a1b6f 52 cid=315cdac35af335e7 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack short-horizon-churn 60ab75751ccdbcae 37 cid=8c082954ad46aa28 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn 60c2747cfb13c22d 2 cid=547d8b0670517664 R1 identity round (harvest Wave-2 R1, 2026-08-24). Five gating rows, ONE change: finding identity now survives a rename or a move. (1) runQualityDelta complexity 201->205 and verbosity 419->462: the +43 lines are overwhelmingly the LEGEND this repo requires — the identity mechanism is a claim about what an ack means, so the report has to say what it rested on (both mechanisms, both truncation caps, the two per-route counters), and legendcoveragecheck arm (A) requires every new attribute be named there. The code added to this dispatcher is three lines: the ledger is now taken from the already-healed basis instead of re-read (re-reading it here would undo the healing), one countAckRescues call, and one call to quality::identityDisclosure — which is itself an EXTRACTION made in this lane precisely because --quality-delta flagged the inline version. The residual +4 complexity is the cid ternary at the ack-write site. Decomposing runQualityDelta is its own round, recorded as such by the W2-E ack above and unchanged by this one. (2) resolveDeltaBasis verbosity 73->86 + churn=self: the two healIdentity calls (ref-pair and working-tree arms) and the comment explaining why the healing must precede computeDelta — heal the baseline after the delta is taken and the delta has already been computed against the stale identity. churn=self on it and on mergescout::injectFileLevelFallback is this one edit window, not a history signal. (3) injectFileLevelFallback: its hand-rolled fopen/fseek/fread/fclose block was retired onto docparse::detail::readWholeFile, the canonical whole-file read, after this lane extracted quality.h's copy and the reuse-decline kind reported the pair as a 320-token clone. Re-read before the swap: short read or ftell failure clears the buffer and the file contributes nothing, byte-identical to what it replaced. Gate: test/identitycheck.sh, 19 arms, 9 RED on the pre-lane binary. +ack short-horizon-churn 60ea298f49a17f57 52 cid=6d3969454bb351ee row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 61d6cde8defa73ad 7 cid=5d0a21a4321d8300 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 61e5df9e1e40ff70 13 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack short-horizon-churn 623e9c51c095e307 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. @@ -914,7 +918,7 @@ ack short-horizon-churn 6b80d3e9ed466af2 5 cid=547ce2e0500da903 OPTREMARKS F3 (d ack short-horizon-churn 6b81837bbce6acb7 2 cid=624eb685c48499de lane/xb-vendored-tier: filter.h is a hot file (>=2 commits/14d repo-wide); short-horizon-churn on isDemoOrGeneratedPath/kDemoOrGeneratedDirs is this edit's own self-churn, not a structural regression ack short-horizon-churn 6bd8a295c4e4d258 6 cid=bef0301883d1faa9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 6df44f90f28d4f64 20 cid=347c2df930b04a16 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently -ack short-horizon-churn 6e58b0a307757079 30 cid=cb5c8aaa7451a632 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. +ack short-horizon-churn 6e58b0a307757079 79 cid=e463a73c1dec022a by=src/* row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn 6e767788a0ca1a2c 2 cid=fc75061b97d51ea4 H13 follow-up: writeStrayEval edited twice in this window (H13 landing, then an XML-comment double-hyphen fix the same finding's legend introduced) — one feature, split into a fix-forward commit rather than an amend, per this repo's never-amend discipline ack short-horizon-churn 7004309695fb79f1 2 finding #7 (2026-08-15 harvest): gitOnlyOmissionNote is the exact site the false 'not a git repository' claim lived at, so closing that finding necessarily edits this already-hot symbol again inside the short-horizon window; the edit is the fix, not incidental thrash ack short-horizon-churn 7135c3324f711fa7 25 cid=3ba05434e6774ff2 2026-09-06 stranger-audit rows 8-12: the two refusals (unreadable root, --cache=) are two guarded calls in dispatchMain; stripHomePair's churn is the two-segment envelope itself @@ -936,7 +940,7 @@ ack short-horizon-churn 7c0e356e60b323ba 41 cid=02317a43a1806043 at-seed decisio ack short-horizon-churn 7cc5608fd1dba918 6 cid=d15d69ecd7b55322 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 7d68e725e85e246c 5 cid=a38b17af7ad94ae5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 7d760d428aab46d1 6 cid=bb2c5ceed4e8efd8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 7e26e5533d486c7a 13 cid=aa596d9bb05d621d cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. | prior: P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical +ack short-horizon-churn 7e26e5533d486c7a 14 cid=a3b42cf8569e33fc row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. ack short-horizon-churn 7ed8ad2c213537a4 24 cid=959b21f28b01efe9 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 7efd6731993172f3 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack short-horizon-churn 7fd074f7cb0b0946 11 cid=0b7c1148e7beac79 lane hb-R3 (harvest-B card C3): the four rows are the disclosure itself. openCacheFrame's 13 refusal branches each name WHICH guard refused a cache artifact — that vocabulary IS the feature, and its LOC and self-churn are the cost of it; runDoctor's self-churn is one added check row; printUsage's +14 lines are the --doctor flag's own documentation, which lives nowhere else. Gate: test/cacheidentitycheck.sh. @@ -975,12 +979,13 @@ ack short-horizon-churn 8cc1013b6962a115 7 cid=d6cf84b967e238c1 OPTREMARKS F3 (d ack short-horizon-churn 8d58de9bb922f582 8 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack short-horizon-churn 8d615258d9601187 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack short-horizon-churn 8dec0ee869bb73ba 7 cid=72d8334527026cac OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 8e587adcc1a3150a 25 cid=07fa36b96d576958 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 900ff4bd8eaab418 7 cid=1359617667336219 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 904831e5c0f66b39 2 cid=ee787c4e01cc4ec2 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. ack short-horizon-churn 90724bf4222d4fb2 5 cid=cfb3c8404d952d3e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 90d3966814ee05fc 114 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn 90d9c6ff7fa34a46 2 cid=9a296cf1115a169f by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. -ack short-horizon-churn 91e4ed97c9ffaea3 16 cid=581fd4bb4ce308c5 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh +ack short-horizon-churn 91e4ed97c9ffaea3 25 cid=4e3104430a276960 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh ack short-horizon-churn 920fb985251704b1 8 cid=ba8429863b7ae2e1 C4 recall budget spreading: recall.h's budget loop was rewritten load-allocate-emit, which necessarily edits committed lines the 2026-09-04 capture-audit round last touched. The churn is real and correctly measured; the edit IS the fix for the defect those lines carry (recallbudgetcheck 8.1/8.2/8.9). Nothing to refactor away. | prior: fix-round follow-on: the recall-ceiling MCP branch (+3 ccx on the dispatcher), the apostrophe word-boundary guards in firstQuotedLiteral (the refusal logic IS the fix), and re-touch churn on the flag table / RecallShape comment / installer timeout re-pin ack short-horizon-churn 925094be92085dae 3 cid=714cea1e1b31a1ae A6: short-horizon churn on editplan::run, ::receipt and ::rollbackMessage is this fix round itself -- four assigned defects on one small surface, committed one per item, so the same three symbols fall inside the churn window more than once. churn=self, not instability in the code. ack short-horizon-churn 93848617cae1fe4e 2 cid=8841d1b1a8bef7a6 M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. @@ -1001,6 +1006,7 @@ ack short-horizon-churn 983814f2b5912a90 41 cid=1ea6676dedf249db at-seed decisio ack short-horizon-churn 9a5e8bcfe860fbd6 2 cid=d289290814db137d by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 9ab981e987e8108e 114 cid=9ea23bc7a7ebc260 lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: VT-1 --run-trace (exec-mode from-trace): churn=self on printUsage/noticeShapingFlagIgnored/main/scanReportVerbPrecedence/fromTraceBundleText is this feature's own edit window — the five surfaces every flag addition touches (help text, budget-notice enumeration, dispatch chain, precedence slots, the shared assembler's additive preludeXml field); verbosity on Config::Config and printUsage is the new flag's config comment + help entry, the documented flag-addition cost. Both real duplication findings this round raised were FIXED by reuse (wsdetail::segmentsOf, any_of), not acked ack short-horizon-churn 9bc4830687767c21 44 W1-S2 dead-code top-level fix: isDeadCandidate param add + the edits to computeDelta/computeSnapshot are the fix itself +ack short-horizon-churn 9c14e69b67463250 52 cid=7f13d2e8aefdfcdc row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 9caaa3dbeaa688d0 16 cid=83d038d1f92d6664 since git-sink hardening (test/sincecheck.sh S0-S4): resolveSinceScope refuses a leading '-' before any git call and resolves a revision through gitResolveCommitSha, and SinceScope drops revBoundary so git log is handed baselineSha, never the caller's string - the task edits exactly these two symbols (churn=self; N4 rewrote them 2026-09-04). The duplication row is the PRE-EXISTING twin prcontext.h::isCommitSha, re-keyed because isBareCommitSha/gitResolveCommitSha moved quality.h -> gitmine.h so resolveSinceScope could call them; consolidating prcontext.h/mergescout.h's resolver copies onto the shared pair is a follow-up lane, not this change ack short-horizon-churn 9e0da9c66a1ac420 39 cid=0a1a678895c18ad1 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack short-horizon-churn 9e62e57560cc1598 4 cid=f76d9e2c555e9b6b Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. @@ -1049,6 +1055,7 @@ ack short-horizon-churn bf2b76f397f48ee2 41 cid=4ab992b78584cb20 at-seed decisio ack short-horizon-churn bf70677c8313a5ec 13 cid=1a2f9c32dfa43545 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn bff7c30779a08bc6 7 cid=dfd6a75f03aff416 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn c00b13f461715002 3 cid=0a077339a7a1e9fc F3 fix round (Finding 5, cross-document recall superset wording) + Finding 7 (this ledger's own reason text): render_recall_pattern is the docs/docs_commands_build.py function that renders the --recall directory-as-knowledge-base subsection; this is its THIRD edit in one afternoon (2026-09-08). bb6a43c2 (13:37) created it; 38970075 (16:09, ~2.5h later — corrected here after this file's ack wrongly said 'three days ago') fixed two defects; this lane's edit adds the qualifier that the section-superset guarantee it documents holds only while the served document SET stays fixed, not across a growing one (own-corpus reproduction: docs/, query 'architecture pipeline evaluation design methodology', --max-tokens=3950->3955 shrinks FIELDAFFINITY.md's own served slice when a 7th document is admitted). Editing the function a third time IS the correction; the churn is the round fixing its own docs commit, not instability in the code. No other kind fires: was=0 now=3. | prior: docs-correction lane (recall passage round): the sole gating row is short-horizon-churn churn=self on render_recall_pattern, the docs/docs_commands_build.py function that RENDERS the --recall directory-as-knowledge-base subsection. bb6a43c2 created that function earlier the same afternoon (2026-09-08, ~2.5h prior) with two defects this lane exists to correct — prose claiming --recall walks 'markdown/text/JSON/etc' (measured false: .txt/.log/.json/extensionless files produce 0 relevant of 0 document files, silently, exit 0) and a KNOWN LIMIT bullet that instructed its own deletion once src/recall.h got a passage-serving fix, which landed in this same tree at a23b7458+1914b7f6. Editing that function a second time in the window IS the correction; the churn is the round fixing its own docs commit, not instability in the code. No other kind fires: was=0 now=2, nothing else in the report gates. +ack short-horizon-churn c0b64aa457bf5776 52 cid=26b94bea0a91f13c row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn c0d15239c2c4709d 24 cid=acf0020a7976fbe1 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn c0e0ed1f0514d6fe 10 cid=bfa2134f1e56ddf1 E1 (terminality round A, lane E): per-op seam disclosure (the plan receipt carries the same two keys as the single-edit receipt) ack short-horizon-churn c16d4b6e3f9eb4c8 6 cid=0184c1e2f4b48bc9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -1058,13 +1065,13 @@ ack short-horizon-churn c3f61f979f7e1278 10 cid=619c397b40760a9f E1 (terminality ack short-horizon-churn c4d50b7393d16274 13 cid=f8caa0a79ac1b73e by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn c5726c667bc4ffa4 5 cid=21bba73836c9ef5a mention_files_capped read a scan STOP as a CUT: a false capped="1" at exactly kMentionMaxFiles matches with any later file, and on every mention after the list filled (one naming nothing, one re-naming a kept file by a longer path, one naming a symbol). The verdict now comes from what each mention NAMES, resolved the way an uncapped scan resolves it (mentionFilesCut -> namesFileNotKept; definesScopeName and namesUnkeptPackageIndex are its (b)/(c) routes). The two self-churn rows are liftPackageDirMention and the applyMentionBoost pass-1 loop going BACK to main shape with the stop-reason reads removed; the api-surface rows are those four helpers. Gated red-first by mentioncapcheck B4-B10; the lift itself is unchanged (mentioncheck ALL PASS). ack short-horizon-churn c5f1ad5fc3a12368 7 cid=c2b6e4d8e9e83369 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh -ack short-horizon-churn c769c8e4d28544b7 37 cid=32cb00692ba5fade lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: L10b finding 1: doc_mentions=/mention_anchored= root attrs on --for (small, bounded conditional growth) +ack short-horizon-churn c769c8e4d28544b7 52 cid=1f30ce78f143a572 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack short-horizon-churn c840f9a6ec2e6c6b 26 W2-F consolidation pass: the same call sites the lane's first commit touched, re-touched to route through the single renderDisclosure() seam. churn=self reads THIS wave's second edit of each; no branch, parameter, symbol or nesting was added — the pass REMOVED four symbols and all four duplication findings ack short-horizon-churn c8702bea42b88461 5 cid=3ced8cf2ed4f9bec OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn c8c7bb0104aa16b8 44 W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d ack short-horizon-churn c8f7a7244861651a 39 cid=3dc6618e7de489b6 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh ack short-horizon-churn c9ff98d0736199ea 3 finding #7 (2026-08-15 harvest): gitOnlyOmissionNote is the exact site the false 'not a git repository' claim lived at, so closing that finding necessarily edits this already-hot symbol again inside the short-horizon window; the edit is the fix, not incidental thrash -ack short-horizon-churn ca97a4b6bf07887b 32 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack short-horizon-churn ca97a4b6bf07887b 79 cid=252f8dcc966e66a4 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn cb5f7d49e698fbaf 13 cid=1078c19d0b21d425 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn cb731eed086b4a0a 13 cid=401c7434ed757c00 by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn cb7342964b38db9c 40 cid=c8da6499c2e07e45 at-seed MCP round: usesText's @-arm (the of= echo split + the atSeedNameOr rebind line) is this lane's own deliberate edit to a symbol that churned only in this same round; complexity/verbosity were already extracted into atSeedNameOr, churn=self is the edit itself @@ -1072,12 +1079,14 @@ ack short-horizon-churn cb9af47b9cf0263d 22 cid=ce433b23e3a5f703 by=src/* rung 3 ack short-horizon-churn ce968bdf8848c214 13 cid=3c5d6c07cbc4425e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d03215f48bec3886 3 cid=c7f3e74cf288dfc2 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper ack short-horizon-churn d05476877df0016e 6 cid=94feac15a58025e5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn d2e94ef1d467cc2b 4 cid=8767f06256890ad8 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn d3afca728392d688 6 cid=5765fc3aca7af273 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d42c85b67bd0956f 28 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) ack short-horizon-churn d44595768a7cf3af 2 cid=21539523996cfb13 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn d557a0077677ebd4 39 cid=7e6e8b041d54e368 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack short-horizon-churn d63db6944aa504a7 35 cid=7d756152fbee4105 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn d6e55b78e3a5fdda 7 cid=31161fe2ed1af5a5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn d721ea3d108a3ba8 79 cid=59552984f192f231 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn d8aaf90801200a68 13 cid=d059a10443da1f80 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d9990dc7492a8bb2 20 cid=4646606f648e387e by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn d9e6c64c181ffc32 8 cid=5fb1e641981216da by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). @@ -1100,7 +1109,7 @@ ack short-horizon-churn e17ff0a2142942c4 4 PHP + Lua language port (lane/lang-ph ack short-horizon-churn e1f67e3817c9c097 7 cid=ff9ddf76360520be OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn e3c54d39d366fbaa 7 cid=402f938df07371e3 M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn e47e183ce409b3d1 5 cid=4a71f6160a322754 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: E3 (terminality round A, lane E): editpreviewcheck's fourth normalisation (strip the preview-only child), stated in the gate -ack short-horizon-churn e514a69013d0934c 88 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites +ack short-horizon-churn e514a69013d0934c 118 cid=62dd3c1f0ae418ce row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack short-horizon-churn e602fc1bbc735406 5 cid=d2100a9e6dc3a52c OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn e6c86967176c9b8f 4 cid=bdc459a986a0a736 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn e6d92432fe4fd546 29 cid=b5e937b02b1a0718 by=src/* markdown/002-counter-saturate lane: the ONE gating row is short-horizon-churn on kParserVer itself, a constant whose entire contract is 'bump on any grammar/.scm/extraction change' — it churns once per grammar lane by design, and its in-window count (29) measures how often this repo changed a grammar, not thrash in this diff. This lane's owned-source footprint is exactly two constant lines (kParserVer 86->87 and quality.h's kIngestParserVerMirror mirroring it, gate-required to move together) plus four vendored C files outside the owned-source metrics. No function's complexity, LOC, nesting, params, duplication, dead-code, api-surface or error-masking moved: regressions=1 and that row IS the version bump. The extraction change it keys was MEASURED, not assumed: map output is byte-identical over 3 538 real files, but a constructed 256-column ATX line goes from n="BuriedHeading" emitted to absent, so a v86 blob can hold a phantom heading. Only the markdown half moves extraction: its indentation counters AND its fence level saturate, both being read by ordering tests against FIXED thresholds. The rust/lua/csharp casts reproduce upstream's value bit-for-bit and contribute nothing to this bump - measured, no extraction difference at any width. | prior: lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). @@ -1111,6 +1120,7 @@ ack short-horizon-churn ec2848a4801493a2 63 L7 lint-catalog: short-horizon-churn ack short-horizon-churn ed0cbdfe7fa72e8c 154 cid=f42bf8683228ffff C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ed10de1685178c0e 12 cid=b144cdefb89bd164 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. ack short-horizon-churn ed4b3f43f7f19909 12 T3 disclosure-gap fix 2026-08-22: verbosity/churn on the two emitters + harness trace persistence are the registered disclosure's own bytes and comments; the gate-helper clone follows the self-contained-MCP-gate convention (every mcp gate carries its own mcp_call) +ack short-horizon-churn ee9d35ace39524e3 2 cid=9631b14801533aea row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn eea83c3db0f03d69 39 cid=95634463181e8692 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack short-horizon-churn efceecf070d71588 6 cid=4d5ad7035c11a2d8 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn f01bb322dbab6ad7 7 cid=ef0d71ff72a335d5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -1120,11 +1130,11 @@ ack short-horizon-churn f34bb3e60088fb42 5 cid=e63de9f5ee69d0f0 OPTREMARKS F3 (d ack short-horizon-churn f3642fd48bc3ef7d 7 short-horizon-churn on kCallHierarchyLegendOpen/runDefaultMap: both symbols sit in main.cpp/graphlegend.h, the two files carrying nearly the whole 2026-08-15 harvest wave (102 and 7 recent commits respectively per git log --since='2 weeks ago') — this fix's own edit is one more necessary touch during an active, intentional multi-lane round, not code churn from an unsettled design. Structurally quiet otherwise (no complexity/nesting/duplication regression on either symbol post-fix). ack short-horizon-churn f3a6e806f38c70a8 6 cid=085fdb748171c083 by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn f3bb371e01471bfb 5 cid=b58d650c34fb3728 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. -ack short-horizon-churn f3ea5dbe8b5720cb 6 cid=8b8d47057aed710b M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. +ack short-horizon-churn f3ea5dbe8b5720cb 52 cid=d4145609e400f052 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. ack short-horizon-churn f43e1f2748e92444 7 cid=0fba983986a21ff4 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn f4dcea0b719bea78 2 cid=8d14b541671382e9 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn f6dedd851e4b2a25 8 cid=282de1477b560a22 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked -ack short-horizon-churn f7cf3b3c2851b336 10 cid=d6c978525c223cb7 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: M1: three named helpers, each replacing an inline duplicate rather than adding surface. mcpVerbDeclaresLegend/mcpCompactLegendHint are the ONE place the compact-legend family and its schema keys are read (mcpCompactLegendHint is not new — it moved down from mcp.h, which includes mcpverbs.h, so the batch assembler can call it); applyCompactToBatchSubs is the shared rule both batch front doors call, CLI and MCP, so the posture cannot fork; appendPackTaskRootExtras splices the bundle's owed root attributes onto BOTH root spellings, which is exactly the bug it prevents. The two ambient-churn rows are the same edit measured by recency. +ack short-horizon-churn f7cf3b3c2851b336 25 cid=9ed90ad0d514832b row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn f8d747d123b4143d 5 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn f99ee113e972ca80 10 cid=5d47fafedace31ba by=src/* M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. | prior: M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack short-horizon-churn fa6dd5532dd57a3c 3 cid=3104c098edd02883 A6: short-horizon churn on editplan::run, ::receipt and ::rollbackMessage is this fix round itself -- four assigned defects on one small surface, committed one per item, so the same three symbols fall inside the churn window more than once. churn=self, not instability in the code. diff --git a/CHANGELOG.md b/CHANGELOG.md index 36d21f470..e3b546433 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,39 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### 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 +pre-change build of the same commit: the flagless map of this repository 26,402 → 22,354 B (−15.3%, the +same 185 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, not bytes: three conceptual and name-exact tasks on this +tree served 25 → 28, 21 → 24 and 3 → 3 signature rows at 10,042 → 10,145, 10,256 → 10,316 and +5,971 → 6,022 B, the difference being the new `sc=`/`route=` reading (259 B, ceiling-droppable with the +confidence clause and exempt from the signature-trim charge like every other disclosure). The `--json` +twins mirror the attribute (`"sc"`), so `mcpattrparity` holds without a rename. Pins moved with the +bytes: 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) and the ten-verb loop 4,900 → 5,000 B, all for the one new whole-document `sc=` reading; the +`test/fixture` map's `est_tokens` 884 → 894; five goldens regenerated for the row shape; the printf-parity +manifest re-pinned for the six labels the rows move. + ### 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 486de35f9..d957733b9 100644 --- a/README.md +++ b/README.md @@ -1797,9 +1797,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-612 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +613 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 **612 gate scripts** and is the authoritative list; +`test/regression.sh` names **613 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 diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a490c710a..6f693b5eb 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1494,7 +1494,7 @@ 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 @@ -1552,7 +1552,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** diff --git a/docs/EVALS.md b/docs/EVALS.md index b93fe69fa..b11bf6ff1 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` | 612 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 613 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 **612 gate scripts**, all of which exist on disk. +naming **613 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 612. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 613. 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/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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 2d53e3bab..82fd532dc 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1068,7 +1068,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["612 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["613 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1092,7 +1092,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "612", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "613", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1342,7 +1342,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["179 long flags · 33 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["612 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["613 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 70 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/cli.h b/src/cli.h index 295fb9320..0f35c51b1 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1213,7 +1213,7 @@ inline constexpr char kHelpHead[] = " FILE:LINE:NAME paste a row's p=\"path:line\" straight from --callers/--lint/--grep\n" " (NOT --hotspots: its p= is a BARE path — build FILE:LINE:NAME from its\n" " own p=/top_l=/top= instead, since top= is just the worst function's name)\n" - " path::scope::name the canonical id= --for/--pack-task emit\n" + " path::scope::name the id composed from a row's p= sc= n= (the map, --for, --pack-task)\n" " Scope::name the sym= spelling edit-check and grep's in= rows print — matches the name\n" " under any scope whose ::-boundary SUFFIX is Scope (Box::lid, deep::Box::lid);\n" " a wrong scope refuses, it never falls back to the bare-name union\n" diff --git a/src/compactlegend.h b/src/compactlegend.h index b8aaa213a..14b794397 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -52,13 +52,13 @@ struct CompactLegendSpec inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { // ── the ranked-map family (root ) — hinted by the map-shaping flag that rode along ── - { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data" }, + { "r", "map", "ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data" }, { "r", "map-diff", "the ranked map anchored at at=: what the diff touched, the map's row vocabulary" }, { "r", "metrics", "the ranked map with per-symbol metrics: in/out, cx/ccx, loc, params, nest, humps/deep, locals, cbo, amp, tested, ev" }, { "r", "around", "call neighbourhood of of=: depth= hops, fanout= kept per hop; absent rows lie outside that boundary" }, { "r", "query", "lexical-rank map for the query term, the map's row vocabulary" }, // ── the bundle family (root ) ── - { "ctx", "pack-signatures", "the ranked map plus signature rows" }, + { "ctx", "pack-signatures", "the ranked map plus signature rows" }, { "ctx", "pack-top-n", "the ranked map plus bodies of the top-N symbols" }, { "ctx", "skipped", "why the index lacks a file ; indexed but unvouched ; census" }, { "ctx", "notes", "field notes by target: holds ; counts = the rows" }, @@ -70,7 +70,7 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] = // 1-hop neighbours in either direction, rel= which one, of_top= there the bodies that qualified (bodiesTotal), shared= how many // of them a row neighbours (emitted above 1); run= rides a row only when a runner is derivable (testmap.h runHint). The // lens facts and route= ride only some answers and are present-only terms below. compactlegendcheck (D36). - { "ctx", "pack-task", "one-call task bundle for task= under budget_tokens=: ranking, ranked but over 1 hop out (of_top= ranked rows) > with callees > 1-hop from the bodies (of_top= bodies; shared= bodies reached, absent at 1) > notes > (run= when derivable)" }, + { "ctx", "pack-task", "one-call task bundle for task= under budget_tokens=: ranking, ranked but over 1 hop out (of_top= ranked rows) > with callees > 1-hop from the bodies (of_top= bodies; shared= bodies reached, absent at 1) > notes > (run= when derivable)" }, { "ctx", "from-trace", "trace frames mapped to indexed symbols, innermost first; the innermost in-corpus body included" }, { "ctx", "exemplar", "the best-in-class instance of kind= for the task, chosen by role: , to imitate" }, { "ctx-partitions", "pack-task", "N minimally overlapping agent bundles carved along call-graph communities plus one shared core; each wraps a " }, @@ -423,7 +423,11 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "ccx", ": cyclomatic/cognitive complexity", true, "d" }, { "in", ": N callers in the index (absent: not measured)", true, "d" }, { "amp", ": direct callers + files sharing a commit with its file (absent at 0)", true, "d" }, - { "route", "route=: the ranker the task was routed to, and why", true, "ctx" }, + { "route", "route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name)", true, "ctx" }, + // row 6 (2026-09-12): the SHORT id on symbol rows. A map row and a lens row carry sc= (the enclosing + // scope) instead of the path-repeating id=; the reading spells the composition once for every root that + // prints the rows, since the shared purposes above only name the attribute. + { "sc", "sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it", true }, { "parse_degraded", "parse_degraded=1: ERROR nodes in that parse", true }, { "tier_partial", "tier_partial=1: tier elected under a partial classification" }, { "dangling", "dangling=1: matches nothing indexed", true }, diff --git a/src/graphlegend.h b/src/graphlegend.h index cf5bb5629..8e69ae1b2 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -276,6 +276,22 @@ inline constexpr const char* kForRootRelAtLegendShort = // `rootOn` is the emitter's own root=-present condition; `atOn` is its at=-present condition (gitAtAttr // non-empty) — never re-derived from each other, since a non-git single-root run has rootOn without atOn. +// Row 6 (2026-09-12): the sc= and route= readings on a --for bundle, ONE spelling for the CLI lens and the MCP +// `for` twin. sc= is the short id (the enclosing scope; a row's full id composes as p::sc::n — see +// serialize.h writeScopeAttr) and route= is a CODE now, not prose: name-exact(X) / subtoken+body[:broad| +// :declined(word;carriers,defs)], with the anchors: clause riding after it on a name-exact route. On the CLI +// lens this clause is CEILING-DROPPABLE (verbs_for.h rung zero, with the confidence and tail clauses): the +// attributes stay on every rung, only the reading goes, and kForLegendDroppedNote names it. No "--" anywhere: +// it rides inside an XML comment, where a double hyphen is ill-formed (G4). +// TERSE ON PURPOSE: this clause rides EVERY default --for answer, and the bytes it costs are the bytes the +// sc= rows and the route code saved (measured 2026-09-12: 20 B per scoped row, 84 B per route note) — a +// reading longer than that saving would make the change a net loss on a small bundle. +inline constexpr std::string_view kForIdRouteLegend = + "; sc=enclosing scope (full id p::sc::n; every selector takes it); route= ranker code: name-exact(X) = the task names X " + "(its anchors list the evidence), subtoken+body = conceptual (:broad = 1-2 plain words, rg may win; :declined(w;carriers,defs) = common-name hit refused)"; +inline constexpr std::string_view kForIdRouteLegendCompact = + "; sc= scope (id=p::sc::n); route= ranker code: name-exact(X)|subtoken+body[:broad|:declined(...)]"; + inline const char* forRootRelPathsLegendShort( bool rootOn, bool atOn = false ) noexcept { if( !rootOn ) diff --git a/src/lexical.h b/src/lexical.h index cedec4f99..dcc5dd3be 100644 --- a/src/lexical.h +++ b/src/lexical.h @@ -2021,9 +2021,10 @@ inline void noteAnchorPlausibility( ImplausibleAnchor& imp, std::string_view low // phrase as a name-exact marker). inline std::string declinedRouteReason( const ImplausibleAnchor& imp ) { - return "subtoken+body BM25 — name-exact declined: anchor '" + imp.word + "' is a common name (" - + std::to_string( imp.carriers ) + " name-carriers, " + std::to_string( imp.defs ) - + " defs); conceptual ranker used"; + // row 6 (2026-09-12): a CODE with the evidence in parentheses — the word that was refused, how many names + // carry it and how many definitions it has; the legend's route= reading spells what :declined means. + return "subtoken+body:declined(" + imp.word + ";" + std::to_string( imp.carriers ) + "-carriers," + + std::to_string( imp.defs ) + "-defs)"; } // split a query on whitespace into raw words (case preserved — camelCase detection needs it) @@ -2141,7 +2142,7 @@ inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view qu if( nameExact ) { rc.which = LexMode::NameExact; - rc.reason = "name-exact BM25 — query names a symbol (" + identifierHit + ")"; + rc.reason = "name-exact(" + identifierHit + ")"; // row 6: a CODE; the reading lives in the legend (route=) // The evidence, appended and never substituted: downstream readers (test/taskechocheck.sh) parse the // clause above out of this same string. A subtoken+body route names no anchors because nothing // anchored it — an anchors list on a route the names did not decide would be evidence after the fact. @@ -2156,12 +2157,12 @@ inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view qu else if( nWords >= 3 ) { rc.which = LexMode::SubtokenBody; - rc.reason = "subtoken+body BM25 (--for's default) — no strong name hit, multi-word conceptual query"; + rc.reason = "subtoken+body"; // row 6: the conceptual ranker, as a code — see the route= legend reading } else { rc.which = LexMode::SubtokenBody; - rc.reason = "subtoken+body BM25 (--for's default) — no strong name hit; broad query, plain rg may also win"; + rc.reason = "subtoken+body:broad"; // row 6: 1-2 plain words — the legend says plain rg may also win } return rc; } diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 6a04769ef..12312f526 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1747,7 +1747,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, noRoute ? std::string() : ( rc.reason + shapeDemotionNote( shape ) ), // row 6: the route CODE, as the CLI twin flRootArg ); // §B1.7: same root attrs as the CLI twin (no route= under no_route, as --no-route) if( !rootOpenStr.empty() && rootOpenStr.back() == '>' ) { @@ -1794,7 +1794,8 @@ inline std::string forTaskText( const std::string& root, const std::string& task 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. @@ -2499,16 +2515,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 @@ -3342,20 +3359,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 @@ -3386,10 +3389,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=. @@ -4640,17 +4644,38 @@ 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 the unmerged row emitted (the merge only ever saves bytes past that charge). +struct MergedCalleeNameRow +{ + std::string_view name; // a view into ing.symbols — stable for the emitter's lifetime + std::string lines; // "203" or "203,206": every definition line of that name in walk order +}; + +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 += ','; r.lines += lb; merged = true; + break; + } + } + if( !merged ) + { + rows.push_back( MergedCalleeNameRow { cs.name, lb } ); + } used += cs.name.size() + 16; if( sink.recorded ) { @@ -4658,6 +4683,19 @@ inline void appendCalleeNameRow( std::string& callsBody, const Symbol& cs, std:: } } +// …and the rows written out, once the block's walk is complete. +inline void appendMergedCalleeNameRows( std::string& callsBody, const std::vector& rows, std::vector& esc ) +{ + for( const MergedCalleeNameRow& r : rows ) + { + 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- @@ -4684,8 +4722,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] ]; @@ -4695,10 +4734,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; } @@ -4723,6 +4762,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 ); } @@ -7188,8 +7228,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( !s.scope.empty() ) { w.write( ",\"sc\":" ); writeJsonStr( w, s.scope, esc ); } // row 6: the XML sibling's sc=, same presence rule if( rows.overloads[ rowIndex ] > 1 ) { rw::formatTo( num, sizeof( num ), ",\"overloads\":{}", rows.overloads[ rowIndex ] ); w.write( num ); } @@ -7405,11 +7444,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( !s.scope.empty() ) // row 6: the XML sibling's sc= — 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/tracelocus.h b/src/tracelocus.h index f0e82de91..c8340c66e 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) diff --git a/src/verbs_for.h b/src/verbs_for.h index c579365eb..06872c9d8 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -145,7 +145,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 = rc.reason + shapeDemotionNote( shape ); // row 6: the code itself, no "routed: " prose prefix 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) @@ -469,6 +469,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 @@ -582,7 +585,7 @@ inline constexpr std::string_view kForCompactConfidenceClause = // 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=) " + " [legend clauses: confidence=/margin_pct=, budget_tokens=/max_tokens=, sc=/route= 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. @@ -595,7 +598,7 @@ inline constexpr std::string_view kForLegendDroppedNote = // 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) - " + " [legend clauses: confidence=/margin_pct=, sc=/route= 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 ) @@ -616,6 +619,10 @@ inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, { h += kForCompactConfidenceClause; } + if( p.idRouteLegend ) + { + h += rw::kForIdRouteLegendCompact; // row 6: sc= and the route= code, the short form; rung zero drops it with the clause above + } if( p.tailLegend ) { h += rw::kForFileTailLegendCompact; // deep-tail: r= + definitions ride the compact legend too, short form @@ -659,7 +666,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"; @@ -711,6 +718,10 @@ inline std::string forLensHeaderText( const ForLensHeaderParts& p, bool withRout h += ": reusable building blocks + quality facts for what you're about to touch " "(cx=complexity ccx=cognitive in=reuse-count churn=recent-commits amp=change-amplification clone=1(duplicated) tested=1) " "— prefer composing/reusing these; watch the high-churn/high-amp/cloned ones"; + if( p.idRouteLegend ) + { + h.append( rw::kForIdRouteLegend ); // row 6: sc= and the route= code — dropped by rung zero with the two clauses below + } // P3 (L7): the r=1 row carries next= (nextverb.h). NOT defined here on purpose: every byte of this header // is un-charged by the token ladder, and fornotesbudgetcheck's tight rungs leave it ~0 tokens of headroom // (V1 N1) — a 115 B clause blew --token-budget=950 by 29 tokens. The definition lives in --help (--for) and @@ -2028,7 +2039,7 @@ std::optional runForLens( const MainDispatch& d ) mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, forConf.attrs, forConf.note, forAtAttrStr, mentionDocAttrsStr, cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", - /*tailLegend=*/true, /*legendDropped=*/false, flRootArg }; + /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg }; const auto buildForHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { return forLensHeaderText( headerParts, withRouteAttr, withTaskEcho, extraNotes ); }; std::string headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); @@ -2285,7 +2296,13 @@ std::optional runForLens( const MainDispatch& d ) // byte-identical with and without it, and charging the clause here would shrink to pay for // a disclosure. The bytes stay real everywhere downstream: est_tokens measures the emitted header, // and the explicit regime's tail rows are funded from the RESIDUAL (below), never from the sigs. - const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted; + // Row 6 (2026-09-12): the sc=/route= reading (graphlegend.h kForIdRouteLegend) joins the exemption on the + // same contract — disclosure only; charging it shrank the explicit-ceiling sig section below the default's + // (forbudgetmonotoncheck #1/#5 caught it, 6507 B against 6141 B at --token-budget=8000). Subtract what was + // EMITTED in this dialect, exactly as the confidence and tail clauses above do. + const std::size_t idRouteLegendEmitted = !headerParts.idRouteLegend ? 0u + : ( compactLegendOn ? rw::kForIdRouteLegendCompact.size() : rw::kForIdRouteLegend.size() ); + const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted; if( exemptBytes > headerStr.size() ) { DEGRADED_PATH_ALERT( "runForLens: header exemptions exceed the emitted header — the sig ledger would underflow; charging the header whole" ); @@ -2644,11 +2661,12 @@ std::optional runForLens( const MainDispatch& d ) // kForLegendDroppedNote, naming the attributes whose definitions just went. Silence here was the // reader seeing confidence= margin_pct= budget_tokens= r= and the counts with nothing in the // legend about any of them and no way to tell a budget cut from a feature that does not exist. - if( !fitsCeiling( headerStr ) && ( !headerParts.confidenceNote.empty() || headerParts.tailLegend ) ) + if( !fitsCeiling( headerStr ) && ( !headerParts.confidenceNote.empty() || headerParts.tailLegend || headerParts.idRouteLegend ) ) { headerParts.confidenceNote = {}; headerParts.tailLegend = false; // deep-tail: the explainer falls with the confidence clause — // the r= attrs and the element (the facts) survive + headerParts.idRouteLegend = false; // row 6: the sc=/route= reading falls with them — the attributes stay headerParts.legendDropped = true; // …and this is the sentence that says both of them fell headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); } diff --git a/test/adaptivecheck.sh b/test/adaptivecheck.sh index cf5b823dd..4ff64059b 100755 --- a/test/adaptivecheck.sh +++ b/test/adaptivecheck.sh @@ -107,7 +107,7 @@ FIXN=$( ls "$FIX" | wc -l | tr -d ' ' ) hdrcut(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE 'adaptive: kept [0-9]+ of [0-9]+[^]]*' | head -1; } # verify-wave2 F6 re-pin: the route value's stop-anchor is the ATTRIBUTE QUOTE, not a trailing "]" — that # bracket was the unbalanced half L10b left behind and is now trimmed at all three construction sites. -routeof(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE 'routed: [^"]*' | head -1; } +routeof(){ "$BIN" "$@" --no-cache 2>/dev/null | grep -oE 'route="[^"]*' | head -1; } SHARPQ="zorbulator telemetry pump" BROADQ="widget" diff --git a/test/anchorfix/golden_for.xml b/test/anchorfix/golden_for.xml index f545e2946..3c89262c2 100644 --- a/test/anchorfix/golden_for.xml +++ b/test/anchorfix/golden_for.xml @@ -1,4 +1,4 @@ -frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()]* n="' \ # ── 2) id= — emitted exactly when scoped, and identical to the DEFAULT map's canonical id ───────────── # The fixture's only scoped symbol is Point (a C++ class); free functions must stay id-less (zero token cost). -MAP_ID="$( grep -o ']*n="Point"[^>]*>' "$TMP/map.xml" | grep -o 'id="[^"]*"' | head -1 )" -FOR_ID="$( drows "$TMP/for.xml" | grep -E ' n="Point"' | grep -o 'id="[^"]*"' | head -1 )" +# row 6 (2026-09-12): the identity attribute is the SHORT id sc= (the enclosing scope) on both surfaces; the +# canonical id composes as p::sc::n, so agreement is "the same sc= for Point" — the path half is the row's own p=. +MAP_ID="$( grep -o ']*n="Point"[^>]*>' "$TMP/map.xml" | grep -o 'sc="[^"]*"' | head -1 )" +FOR_ID="$( drows "$TMP/for.xml" | grep -E ' n="Point"' | grep -o 'sc="[^"]*"' | head -1 )" if [ -n "$MAP_ID" ]; then [ "$MAP_ID" = "$FOR_ID" ] \ - && ok "id= agrees with the default map's canonical id for Point ($MAP_ID)" \ - || no "id= disagrees with the default map: map=$MAP_ID bundle=${FOR_ID:-}" + && ok "sc= agrees with the default map's scope for Point ($MAP_ID)" \ + || no "sc= disagrees with the default map: map=$MAP_ID bundle=${FOR_ID:-}" else - no "default map emitted no id= for Point — the fixture changed; re-anchor this assertion" + no "default map emitted no sc= for Point — the fixture changed; re-anchor this assertion" fi -SCOPELESS_WITH_ID="$( drows "$TMP/for.xml" | grep -E ' n="(distance|perimeter)"' | grep -c 'id="' )" +SCOPELESS_WITH_ID="$( drows "$TMP/for.xml" | grep -E ' n="(distance|perimeter)"' | grep -c 'sc="\| id="' )" [ "$SCOPELESS_WITH_ID" = "0" ] \ - && ok "id= omitted on scope-less symbols (canonical id == bare name → no token cost)" \ + && ok "sc= omitted on scope-less symbols (canonical id == bare name → no token cost)" \ || no "id= emitted on $SCOPELESS_WITH_ID scope-less row(s) — it must add disambiguation or be absent" # ── 3) --for and --pack-task agree on in= for every symbol BOTH bundles name ────────────────────────── diff --git a/test/candheadcheck.sh b/test/candheadcheck.sh index 3f0f42c60..b1ba5503d 100755 --- a/test/candheadcheck.sh +++ b/test/candheadcheck.sh @@ -119,7 +119,7 @@ else fi # ── (f) ROUTE SCOPE ─────────────────────────────────────────────────────────────────────────────────── -grep -q 'routed: name-exact' "$TMP/lens" \ +grep -q 'route="name-exact(' "$TMP/lens" \ && ok "--for=Frobnicator takes the name-exact route (the route this fix's arms measure)" \ || no "--for=Frobnicator no longer routes name-exact" diff --git a/test/canoncheck.sh b/test/canoncheck.sh index 4131d7bdf..1ab96745f 100755 --- a/test/canoncheck.sh +++ b/test/canoncheck.sh @@ -34,17 +34,19 @@ if diff -q "$TMP/a" "$TMP/b" >/dev/null; then ok "determinism (byte-identical, $ MAP="$( "$BIN" "$CORPUS" --no-cache 2>/dev/null )" # 2) DISTINCT canonical ids — A::compute and B::compute each get their own `id=…::::compute`. -if printf '%s' "$MAP" | grep -q 'id="[^"]*canon.cpp::A::compute"'; then ok "canonical id present: …::A::compute"; else { no "no canonical id …::A::compute"; printf ' %s\n' "$MAP"; }; fi -if printf '%s' "$MAP" | grep -q 'id="[^"]*canon.cpp::B::compute"'; then ok "canonical id present: …::B::compute"; else { no "no canonical id …::B::compute"; printf ' %s\n' "$MAP"; }; fi +# row 6 (2026-09-12): the row prints the short id sc= (the enclosing scope); the canonical id composes as p::sc::n +# with the enclosing , so "distinct canonical ids" is "same n=, distinct sc=" under the one canon.cpp file. +if printf '%s' "$MAP" | grep -q 'n="compute" sc="A"'; then ok "canonical id present: …::A::compute (n=compute sc=A)"; else { no "no canonical id …::A::compute"; printf ' %s\n' "$MAP"; }; fi +if printf '%s' "$MAP" | grep -q 'n="compute" sc="B"'; then ok "canonical id present: …::B::compute (n=compute sc=B)"; else { no "no canonical id …::B::compute"; printf ' %s\n' "$MAP"; }; fi # the two ids are genuinely DIFFERENT strings (the whole point: a same-name collision now disambiguates) -A_ID="$( printf '%s' "$MAP" | grep -o 'id="[^"]*canon.cpp::A::compute"' | head -1 )" -B_ID="$( printf '%s' "$MAP" | grep -o 'id="[^"]*canon.cpp::B::compute"' | head -1 )" +A_ID="$( printf '%s' "$MAP" | grep -o 'n="compute" sc="A"' | head -1 )" +B_ID="$( printf '%s' "$MAP" | grep -o 'n="compute" sc="B"' | head -1 )" if { [ -n "$A_ID" ] && [ -n "$B_ID" ] && [ "$A_ID" != "$B_ID" ]; }; then ok "the two compute() defs have DISTINCT canonical ids ($A_ID != $B_ID)"; else no "the two compute() ids are not distinct (A='$A_ID' B='$B_ID')"; fi # 3) a free-function-style collision is not the case here, but verify id= is ONLY on scoped symbols: # every emitted id= must contain '::' (a scope) — never a bare name (that would be redundant churn). -BAD_ID="$( printf '%s' "$MAP" | grep -o 'id="[^"]*"' | grep -v '::' | head -1 )" -if [ -z "$BAD_ID" ]; then ok "id= emitted only when it disambiguates (every id= is scoped, none == bare name)"; else no "a bare (scope-less) id= leaked: $BAD_ID"; fi +BAD_ID="$( printf '%s' "$MAP" | grep -o 'sc=""\| id="[^"]*"' | head -1 )" +if [ -z "$BAD_ID" ]; then ok "sc= emitted only when it disambiguates (never empty, and no path-repeating id= on a row)"; else no "an empty sc= or a path-repeating id= leaked: $BAD_ID"; fi # 4) the member call resolves to A::compute ONLY (canonical scope + locality), NOT B::compute. # --callees lists the resolved out-edges with file:line; A::compute is line 26, B::compute is line 31. diff --git a/test/chainidcheck.sh b/test/chainidcheck.sh index 5be21124e..756aa87e2 100755 --- a/test/chainidcheck.sh +++ b/test/chainidcheck.sh @@ -23,11 +23,18 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } echo "chainidcheck: BIN=$BIN" # 1) harvest a REAL scoped id straight out of a --for bundle — never a hand-written literal, so the gate -# keeps testing the actual emitted shape if the id format ever changes. -ID="$( "$BIN" . --for="serialize xml writer" 2>/dev/null \ - | grep -oE ']*id="[^"]+"' | head -1 | grep -oE 'id="[^"]+"' | sed 's/id="//;s/"$//' )" -if [ -n "$ID" ] && [ "${ID#*::}" != "$ID" ]; then ok "harvested a scoped id from --for ($ID)" -else no "no scoped id= in --for output — lane F's chain key is missing"; echo "ALL FAIL"; exit 1; fi +# keeps testing the actual emitted shape if the id format ever changes. Row 6 (2026-09-12): the row prints +# the SHORT id (sc=, the enclosing scope) beside its own p=; the chain key composes as p::sc::n, exactly +# the spelling the legend states and the selectors accept. +ID="$( "$BIN" . --for="serialize xml writer" 2>/dev/null | python3 -c ' +import re, sys +for row in re.finditer( r"]*)>", sys.stdin.read() ): + a = dict( re.findall( r"\s([\w:.-]+)=\"([^\"]*)\"", row.group( 1 ) ) ) + if "sc" in a and "p" in a: + print( a[ "p" ] + "::" + a[ "sc" ] + "::" + a[ "n" ] ); break +' )" +if [ -n "$ID" ] && [ "${ID#*::}" != "$ID" ]; then ok "composed a scoped id from a --for row's p= sc= n= ($ID)" +else no "no scoped sc= row in --for output — lane F's chain key is missing"; echo "ALL FAIL"; exit 1; fi # 2) producer -> consumer: that id resolves on each nav verb (exit 0 AND the verb echoes it back). for v in expand callers impact uses; do diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh index 34b8c932a..302fa5eb4 100755 --- a/test/childwalkscalecheck.sh +++ b/test/childwalkscalecheck.sh @@ -414,7 +414,7 @@ else no "(A11) ffiVisitNode/linkage: the linkage string was not found past the flood — the extern \"C\" call edge is gone" fi "$BIN" "$TMP/pyenum/n1000" --no-cache --top-k=100000 >"$TMP/a_pyenum.xml" 2>/dev/null -if [ "$( count_rows "$TMP/a_pyenum.xml" ' rows, --lego's caveat=; on the map family and --impact: pr_iters=, @@ -331,12 +331,18 @@ probeFor() # (no probe on this fixture prints one) and shortened fourteen readings without losing accuracy. Ten pins moved down. # Re-measured at the lane's end, (D36): pack-task 327 -> 804 B (its purpose line spells the bundle's own vocabulary, and the # and route= readings ride its rows) and from-trace 290 -> 445 B (the same four readings); no other schema moved. +# RE-PINNED 2026-09-12 (row 6, sc= — the short symbol id): seven schemas moved up by the ONE new whole-document reading +# `sc=: enclosing scope; the full id is p::sc::n …` (~95 B) that every root printing / rows now carries, plus the +# route= reading's growth on the bundle family (route= is a CODE now and the reading spells the codes): map 810 -> 920 +# (measured 908, the --rank-by=churn probe), map-diff 800 -> 910 (901), pack-signatures 680 -> 780 (775), metrics 720 -> 820 +# (814), query 630 -> 730 (723), pack-task 820 -> 980 (974), pack-top-n 660 -> 770 (761). The bytes the reading costs are the +# bytes the rows save: 20 B per scoped row (this repo's flagless map, 137 scoped rows: -4,048 B, -15.3%). # schema pin measured PIN_TABLE=' -ripwire.map/v1 810 799 -ripwire.map-diff/v1 800 789 -ripwire.pack-signatures/v1 680 663 -ripwire.metrics/v1 720 702 +ripwire.map/v1 920 908 +ripwire.map-diff/v1 910 901 +ripwire.pack-signatures/v1 780 775 +ripwire.metrics/v1 820 814 ripwire.deps/v1 260 245 ripwire.hotspots/v1 280 264 ripwire.clones/v1 290 280 @@ -377,7 +383,7 @@ ripwire.mentions/v1 180 168 ripwire.affected/v1 350 339 ripwire.verify/v1 330 316 ripwire.help-task/v1 170 153 -ripwire.query/v1 630 611 +ripwire.query/v1 730 723 ripwire.grep/v1 360 345 ripwire.match/v1 270 260 ripwire.lego/v1 290 275 @@ -395,8 +401,8 @@ ripwire.merge-scout/v1 220 208 ripwire.whereis/v1 240 223 ripwire.community/v1 730 719 ripwire.layout/v1 160 149 -ripwire.pack-task/v1 820 804 -ripwire.pack-top-n/v1 660 649 +ripwire.pack-task/v1 980 974 +ripwire.pack-top-n/v1 770 761 ripwire.expand/v1 280 265 ' pinFor() @@ -508,10 +514,13 @@ echo # RE-ANCHORED 2026-09-12 (the fourth sweep's last pass): 4,400 → 4,900 B, measured 4,860 (from 4,392), by the same rule. Attributed # against the previous lane build: --impact 597 → 777 B (its importer-row reading) and --safe-delete 424 → 712 B (t=/p=, # defs=, ambiguous_callers=, dead_code_candidate=); nothing else in the loop moved. +# RE-ANCHORED 2026-09-12 (row 6, sc= — the short symbol id): 4,900 → 5,000 B, measured 4,946 (from 4,849), by the same rule. Attributed: +# the one new whole-document reading `sc=: enclosing scope; the full id is p::sc::n …` rides every loop verb whose answer prints +# a scoped symbol row (~95 B each where present) — the bytes the rows themselves give back at 20 B per scoped row. # RE-MEASURED 2026-09-12 (that sweep's design review): 4,849 B, the pin unchanged at 4,900. Attributed against the last-pass build: # --impact 777 → 770 B (the shorter reading) and --safe-delete 712 → 708 B (t= reads a match, dead_code_candidate= says # outside); nothing else in the loop moved. -echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 4,900 B (29,824 B in full on the ripwire tree) ===" +echo "=== (L) the canonical ten-verb edit loop: compact legend bill ≤ 5,000 B (29,824 B in full on the ripwire tree) ===" loopBytes=0; fullBytes=0 for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--uses=distance" "--edit-check=total_area" \ "--quality-delta" "--test-gate=geometry.cpp" "--affected=geometry.cpp" "--safe-delete=total_area" "--slice=total_area"; do @@ -520,8 +529,8 @@ for v in "--for=geometry distance" "--callers=distance" "--impact=distance" "--u b="$( leg bytes "$TMP/l.c" )"; f="$( leg bytes "$TMP/l.f" )" loopBytes=$(( loopBytes + b )); fullBytes=$(( fullBytes + f )) done -[ "$loopBytes" -le 4900 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ - || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 4,900 B; full: $fullBytes B)" +[ "$loopBytes" -le 5000 ] && ok "(L) ten-verb loop: $loopBytes B of compact legend (full: $fullBytes B)" \ + || no "(L) ten-verb loop pays $loopBytes B of compact legend (> 5,000 B; full: $fullBytes B)" echo echo "=== (M) MCP: legend:\"compact\" on edit_check answers in ≤ 900 B on a clean tree; every XML verb takes the argument, within its per-verb legend pin ===" diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index 22ec9c407..65ee55fed 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -497,10 +497,10 @@ DEFCALLEES="$( run "$DEFFIX" --callees=deep2 --no-cache )" # PRE-FIX binary, minted by the in-class DECLARATIONS — so an id-presence arm passes while the two # definitions are missing entirely. The discriminating fact is which row the def MERGED INTO, read off # `overloads=` (decl+def collapsed = 2) and off the caller line each sink names. -printf '%s' "$DEFMAP" | grep -qE '/dev/null | grep -q 'routed: name-exact' \ + "$BIN" defoverdeclfix --for="$q" 2>/dev/null | grep -q 'route="name-exact(' \ && ok "--for=$q takes the name-exact route (the route this rule is scoped to)" \ || no "--for=$q no longer routes name-exact — the arms above are measuring the wrong ranker" done @@ -129,7 +129,7 @@ done # inert arm in a gate written for a ranking change is the failure mode this suite has been bitten by # twice. The first row is also the one that survives every budget: it is what the defect actually # costs on a corpus where 85 declarations push the definition off the end. -first="$( tr '>' '\n' <"$TMP/lens" | sed -n 's/.*/dev/null | grep -q 'routed: subtoken+body' \ +"$BIN" defoverdeclfix --for="$CQ" 2>/dev/null | grep -q 'route="subtoken+body' \ && ok "the declaration-seeking conceptual query still routes subtoken+body" \ || no "the declaration-seeking conceptual query changed route" want='a_headers.hpp::Sprocket::Sprocket diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 431412ac1..12410287a 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -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..6d5ac51e2 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/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/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..079adfd20 100755 --- a/test/fillordercheck.sh +++ b/test/fillordercheck.sh @@ -112,7 +112,10 @@ 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. +{ [ "$EFIX" = "894" ] && [ "$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/golden.xml b/test/golden.xml index 3185db791..0a03e5de3 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/mcpforparitycheck.sh b/test/mcpforparitycheck.sh index 4d1bf6cfc..909728f9c 100755 --- a/test/mcpforparitycheck.sh +++ b/test/mcpforparitycheck.sh @@ -225,7 +225,11 @@ done # (the first returns -32602 "unknown field: 'no_route'"). mcp_for_nr(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s","no_route":%s}}}\n' \ "$CORPUS" "$1" "$2" | "$BIN" --mcp 2>/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" 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/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/printf_parity.manifest b/test/printf_parity.manifest index 3ea144ad8..256591997 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -1,4 +1,4 @@ -flagless 0 4fbbea0a8d22d1633cc7b0469ffbe75cd3775c0065c8982015a539ad5d08a06e e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +flagless 0 e7a5fbb7a683fa5edb35b38695d9ff1f027cd28401881f1bef9391754e87dd79 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 3290aebfaa3f6c4152c17a761f453ad2d79ad28147c4faf89a83a3832decee3d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -expand 0 031a0bef5b4dbd5f548ce89d76928b132b7249b26ab540b1896d19e9f2c7d29d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +expand 0 200460ffdd5cd804a5c987d000844a91637776fc88c42cfcbb3f7888c63c0501 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 f397f2886049eb96b4bd3028d8108c1624e3959f2d428ca4687b0005144f3154 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_signatures 0 cd9324abaee039376e3e19981d56cd83cf9a758741b3bfd073575379daea0f6d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_task 0 519b456fa804af76cb8011937c7e56a7542b0ad84a2ff57f54b442e853d815f7 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 fc6cdaff9da1b3af90fa3ad4bd4511da5cc23b27d24105ab18f73d1e53a28109 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 2a1caf5519f66891ba082520f2342fb1a26678043cccebe20ac0d8b2b1e0d841 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 324ea929a..6db27807f 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 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 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 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 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 scroundtripcheck 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 [ -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..6423e5a87 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -112,28 +112,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 +168,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 +240,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 +298,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 +340,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/routefix/golden_for.xml b/test/routefix/golden_for.xml index 9521180dd..126ab4fc6 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()"$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 '\|\| 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 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/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/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' )" From 761c45b83a7ba779bc520d9809d4cd377e8afbba Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sat, 12 Sep 2026 22:28:21 -0400 Subject: [PATCH 02/22] perf(for): the compact legend --for answered with was the default dialect's sentences behind a schema id; it is present-only now and pinned at 500 B MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE DEFECT. Every other XML verb under --legend=compact defines only the terms its document carries, measured and pinned per schema in test/compactlegendcheck.sh; --for did not. Its native compact dialect spent 1,177-1,216 B on that gate's own fixture — the default legend's sentences with ripwire.for/v1 in front — and was exempt from the per-verb pin by name ("--for keeps its native dialect and is exempt"). A compact dialect exempt from being compact is not one, on the verb an agent calls more than any other. THE FIX (A1', owner decision 2026-09-12; the CLI default stays full). appendCompactForLegend emits one present-only comment: a reading per row and root attribute the bundle prints (cx/ccx/in/churn/ amp/clone/tested, sc= and the route= code, bundle/bodies/reason, the total=/shown=/capped= window, the hops clause on the compact serving shape or the bodies clause on the auto shape, the tail, the confidence gauge); route= is defined only when the root carries it (the ladder's rung (c) drops the attribute and its reading with it); the data notes keep their numbers and lose their sentences (compactForNote: "[floor: kept 7 of 40]", "[doc mentions: 1 doc, 1 symbol; doc_mentions=]", "[mention anchor: 1 file + 2 symbols; mention_anchored=]"; adaptive/siblift/expansion notes stay verbatim, their text IS the data; an unknown note passes through untouched). The three ceiling-droppable clauses still fall together under a tight --token-budget and kForLegendDroppedNoteCompact still names them. The sig-trim exemption subtracts what this dialect EMITS (kForCompactLegendConfidence/Tail/Route), the rule P1 (L7) set when the full constants left the ledger 64 B short; the three superseded compact constants (kForCompactConfidenceClause, kForFileTailLegendCompact, kForIdRouteLegendCompact) are removed with their last reader. MEASURED. The gate's own splitter (comment bytes not present verbatim in the default document) on its fixture probe --for=geometry: 915 -> 494 B; pinned at 500 as the new ripwire.for/v1 row, the two exemptions (the "shrinks vs full" stand-in for a pin, and the union with the full legend on the completeness-attribute arm) removed. Per call on this tree, three tasks under --legend=compact: 9,947 -> 9,669, 10,075 -> 9,682, 5,156 -> 4,624 B (-278, -393, -532) with the same signature rows served (32, 26, 3). The MCP `for` twin declares no legend field and serves the default dialect only: unchanged at 9,132, 9,149 and 2,173 B on the same tasks. A doc-mentions probe on the fixture measures 541 B (the note is data; reported, not hidden). GATE, RED FIRST. test/compactlegendcheck.sh with the ripwire.for/v1 500 row against the previous commit's binary: "(U) --for=geometry compact PROSE legend is 915 B (> its 500 B pin)"; against this build ALL PASS. legendcoveragecheck holds (every first-screen attribute of the compact document has a name= definition in the one comment; next=/pure=/schema= stay on the recorded floor as before); 32 --for/legend/parity gates green (estcharge, fornotesbudget, forcompress, mcpclidiff, routeonce, forbudgetmonoton, fordisclosure, forrootlegend, estcalib, mcpforparity, mcpattrparity, compactroute, deeptail, docscommands, taskecho, legendcost, w3fixlegend, formaxtokens, tokenbudget, fornotesjson, forlens, forautobody, droppedpositive, relevancefloor, docmention, mention, adaptive, batch, scroundtrip, xmlwellformed, printffmtparity). ASan clean on the compact conceptual, name-exact, budgeted and doc-mentions paths; determinism and xmllint hold. quality-delta gating=0 after acking the one deliberate contract change (withRouteAttr, one caller) and this lane's own churn. Pins moved: ripwire.for/v1 added at 500 (measured 494); none moved. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 7 +- CHANGELOG.md | 22 ++++++ src/graphlegend.h | 2 - src/serialize.h | 5 -- src/verbs_for.h | 134 ++++++++++++++++++++++++++++--------- test/compactlegendcheck.sh | 25 +++---- 6 files changed, 143 insertions(+), 52 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index d460f1320..dbcb5e2f0 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -142,6 +142,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=1eba8698198fbb41 A1' (2026-09-12): appendCompactForLegend takes withRouteAttr so the compact --for legend can define route= only when the root carries it (the ladder's rung (c) drops the attribute); one caller, forLensHeaderText, updated in the same commit 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 @@ -890,7 +891,7 @@ ack short-horizon-churn 572dbeee3b452cbc 21 cid=e0a24e18a683a9b1 by=src/* M12 (c ack short-horizon-churn 5774f0f445361430 8 graphrag-recon idea #1 corroboration-weighted callers: renderD1CallerRows' own short-horizon-churn is the direct, expected, unavoidable cost of implementing the feature in its territory (src/packtask.h caller-section renderer) -- not a code-shape regression ack short-horizon-churn 578fa051307418ee 5 cid=307a821a72c35f00 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 59f050855874875f 14 cid=dbe0701cc173e389 by=src/* R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. -ack short-horizon-churn 5b224c7fe142bd56 52 cid=894118d22b83c03f by=src/* row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked +ack short-horizon-churn 5b224c7fe142bd56 53 cid=4bdde712d4c40f4d by=src/* A1' (2026-09-12): the compact --for legend is rebuilt present-only at 500 B — appendCompactForLegend and runForLens's exemption arithmetic are this lane's own edits, not thrash | prior: row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn 5c2c4a2b311b8dba 5 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 5d8f5288f0df10f7 95 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact ack short-horizon-churn 5e16b9596d9bd135 45 cid=743b6f0afb91f3cc by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh @@ -1065,7 +1066,7 @@ ack short-horizon-churn c3f61f979f7e1278 10 cid=619c397b40760a9f E1 (terminality ack short-horizon-churn c4d50b7393d16274 13 cid=f8caa0a79ac1b73e by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn c5726c667bc4ffa4 5 cid=21bba73836c9ef5a mention_files_capped read a scan STOP as a CUT: a false capped="1" at exactly kMentionMaxFiles matches with any later file, and on every mention after the list filled (one naming nothing, one re-naming a kept file by a longer path, one naming a symbol). The verdict now comes from what each mention NAMES, resolved the way an uncapped scan resolves it (mentionFilesCut -> namesFileNotKept; definesScopeName and namesUnkeptPackageIndex are its (b)/(c) routes). The two self-churn rows are liftPackageDirMention and the applyMentionBoost pass-1 loop going BACK to main shape with the stop-reason reads removed; the api-surface rows are those four helpers. Gated red-first by mentioncapcheck B4-B10; the lift itself is unchanged (mentioncheck ALL PASS). ack short-horizon-churn c5f1ad5fc3a12368 7 cid=c2b6e4d8e9e83369 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh -ack short-horizon-churn c769c8e4d28544b7 52 cid=1f30ce78f143a572 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked +ack short-horizon-churn c769c8e4d28544b7 53 cid=303f15bbfd0d477c A1' (2026-09-12): the compact --for legend is rebuilt present-only at 500 B — appendCompactForLegend and runForLens's exemption arithmetic are this lane's own edits, not thrash | prior: row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn c840f9a6ec2e6c6b 26 W2-F consolidation pass: the same call sites the lane's first commit touched, re-touched to route through the single renderDisclosure() seam. churn=self reads THIS wave's second edit of each; no branch, parameter, symbol or nesting was added — the pass REMOVED four symbols and all four duplication findings ack short-horizon-churn c8702bea42b88461 5 cid=3ced8cf2ed4f9bec OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn c8c7bb0104aa16b8 44 W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d @@ -1130,7 +1131,7 @@ ack short-horizon-churn f34bb3e60088fb42 5 cid=e63de9f5ee69d0f0 OPTREMARKS F3 (d ack short-horizon-churn f3642fd48bc3ef7d 7 short-horizon-churn on kCallHierarchyLegendOpen/runDefaultMap: both symbols sit in main.cpp/graphlegend.h, the two files carrying nearly the whole 2026-08-15 harvest wave (102 and 7 recent commits respectively per git log --since='2 weeks ago') — this fix's own edit is one more necessary touch during an active, intentional multi-lane round, not code churn from an unsettled design. Structurally quiet otherwise (no complexity/nesting/duplication regression on either symbol post-fix). ack short-horizon-churn f3a6e806f38c70a8 6 cid=085fdb748171c083 by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn f3bb371e01471bfb 5 cid=b58d650c34fb3728 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. -ack short-horizon-churn f3ea5dbe8b5720cb 52 cid=d4145609e400f052 row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash | prior: M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. +ack short-horizon-churn f3ea5dbe8b5720cb 53 cid=1eba8698198fbb41 A1' (2026-09-12): the compact --for legend is rebuilt present-only at 500 B — appendCompactForLegend and runForLens's exemption arithmetic are this lane's own edits, not thrash | prior: row 6 (2026-09-12): sc= short ids, route= codes and merged callee rows touch the four emitters every legend lane of this week touched (serialize, runForLens, forTaskText, kCompactLegendSpecs); the churn is the lane's own edit, not thrash ack short-horizon-churn f43e1f2748e92444 7 cid=0fba983986a21ff4 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn f4dcea0b719bea78 2 cid=8d14b541671382e9 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn f6dedd851e4b2a25 8 cid=282de1477b560a22 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked diff --git a/CHANGELOG.md b/CHANGELOG.md index e3b546433..25ec99bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,28 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — `--for`'s compact legend is pinned at 500 bytes + +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 → 494 B, pinned at 500 as the new +`ripwire.for/v1` row, the exemption gone. Per call on this tree, three tasks under `--legend=compact`: +9,947 → 9,669, 10,075 → 9,682 and 5,156 → 4,624 B (−278, −393, −532 B) with the same signature rows +served (32, 26, 3). The MCP `for` twin declares no `legend` field and serves the default dialect only, +so its bytes are unchanged (9,132, 9,149 and 2,173 B on the same tasks). `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` diff --git a/src/graphlegend.h b/src/graphlegend.h index 8e69ae1b2..3477871d2 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -289,8 +289,6 @@ inline constexpr const char* kForRootRelAtLegendShort = inline constexpr std::string_view kForIdRouteLegend = "; sc=enclosing scope (full id p::sc::n; every selector takes it); route= ranker code: name-exact(X) = the task names X " "(its anchors list the evidence), subtoken+body = conceptual (:broad = 1-2 plain words, rg may win; :declined(w;carriers,defs) = common-name hit refused)"; -inline constexpr std::string_view kForIdRouteLegendCompact = - "; sc= scope (id=p::sc::n); route= ranker code: name-exact(X)|subtoken+body[:broad|:declined(...)]"; inline const char* forRootRelPathsLegendShort( bool rootOn, bool atOn = false ) noexcept { diff --git a/src/serialize.h b/src/serialize.h index d6e1df06c..16a217f02 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -996,11 +996,6 @@ inline constexpr std::string_view kForFileTailLegend = "NOT among the shown sigs rows — the files of trimmed rows first, best-symbol rank order; rows are t p=file; total=such files, " "shown=printed, capped=1 when they differ. r= on a ranked row is its 1-based rank in this lens ranking, " "rows in r= order, p= the file (a gap = a budget-trimmed row)"; -// P1 (L7): the same two definitions for the compact dialect (verbs_for.h appendCompactForLegend) — nothing dropped, -// the sentences shortened: the tail is file-grain and weaker, its counts are total/shown/capped, r= is the rank. -inline constexpr std::string_view kForFileTailLegendCompact = - "; tail: file-grain tail (paths only, WEAKER than the ranked rows): every positive-score file not among the shown sigs rows, trimmed rows' files first; rows, total=/shown=/capped=1 when cut; " - "r= = a ranked row's 1-based lens rank, rows in r= order, p= the file (a gap = a budget-trimmed row)"; // Explicit-budget row fit: the largest shown count whose rendered XML fits `budgetBytes` (0 rows always // "fits" — the shell is reserved by the caller). Walks down from the collected count; deterministic. diff --git a/src/verbs_for.h b/src/verbs_for.h index 06872c9d8..95c7ff2f9 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -539,11 +539,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; @@ -591,46 +586,123 @@ inline constexpr std::string_view kForLegendDroppedNote = // …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 +// there. The compact dialect emits kForCompactLegendConfidence 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 +// reason kForCompactLegendConfidence 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=, sc=/route= 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 ) +// A1′ (owner decision 2026-09-12): --for's COMPACT legend, pinned at 500 B (test/compactlegendcheck.sh, the +// ripwire.for/v1 row) and PRESENT-ONLY — every clause below is emitted only when the document carries the +// attribute it defines, and every reading is the shortest honest one. It used to spend 1,177–1,216 B here +// (the same sentences as the default dialect with a schema id in front) and was exempt from the per-verb +// pin by name; a compact dialect exempt from being compact is not one. The data notes (relevance floor, +// doc mentions, mention anchor, co-change boost) keep their NUMBERS and lose their explanation +// (compactForNote below); adaptive/siblift/expansion notes are kept verbatim — their text IS the data +// (cliff rank, drop, the env knob). No "--" anywhere: an XML comment (G4). +// +// WHAT RIDES WHERE. Root facts that the root's own attributes state (bundle=/bodies=/reason=) get one +// clause; the lens columns one clause (cx/ccx/in/churn/amp/clone/tested are always computed on this +// surface, clone=/tested= appear only where 1); sc= and next= ride the clause; route= only when the +// root carries it (the ladder's rung (c) drops the attribute, and then its reading goes with it); the +// confidence, sc=/route= and tail clauses are the ceiling-droppable trio (rung zero, kForLegendDroppedNoteCompact +// names them); the compact bundle's hops/calls/noedge clause and the auto bundle's bodies clause are +// mutually exclusive by construction (planForEnrichment). +inline constexpr std::string_view kForCompactLegendRoot = + "bundle=/bodies=/reason= the body posture; "; +inline constexpr std::string_view kForCompactLegendRows = + "d: cx= ccx= complexity, in= callers, churn= amp= change, clone= tested= 1, sc= scope, id=p::sc::n; " + "total= shown= capped=1 if cut"; +inline constexpr std::string_view kForCompactLegendRoute = + "; route= name-exact(X)|subtoken+body[:broad|:declined]"; +inline constexpr std::string_view kForCompactLegendConfidence = + "; confidence=/margin_pct= head score drop (low=flat)"; +inline constexpr std::string_view kForCompactLegendHops = + "; h l= p= n=, c n= l= (joined for same-named callees, shown= counts them), noedge= no callee resolved"; +inline constexpr std::string_view kForCompactLegendBodies = + "; b t= n= p= l= full bodies, c n= l= callee signatures"; +inline constexpr std::string_view kForCompactLegendTail = + "; t p= file outside sigs (weaker), r= rank (gap = trimmed)"; + +// The data notes in their compact spelling: the numbers stay, the sentence goes. Unknown shapes pass +// through VERBATIM — a note this table does not know is never shortened into something it did not say. +inline std::string compactForNote( std::string_view note ) { - h += ""; // M10: at= folded into the SAME trailing comment as root= (one wrapper, not two) — the @@ -688,7 +760,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 += "frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()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 6d5ac51e2..4e1851652 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 ): is byte-shaped, so every id= the rows no longer repeat -# became a signature row (q10: 23 -> 27 rows). Nothing here is corpus drift — the two trees agree to ±1 B under -# one binary — and the drift warning above still stands for the four left in place. -REPO_BASE=( 9981 9961 10217 10440 9755 9949 10357 10270 10092 10327 ) +REPO_BASE=( 9981 9961 9784 9968 9755 9949 9909 9745 9613 9806 ) # ── (1)+(2) rank order + p= on every row, four dialects ─────────────────────────────────────────────────── order_fail=0 @@ -160,7 +146,7 @@ FX_Q=( "geometry area of a shape" "call a native function from python" "parse th # ── (3) byte growth ≤ 4% against the registered sizes ───────────────────────────────────────────────────── growth_fail=0 -echo " ledger: the ten reference queries (this repo, full legend) — base bytes @8eb669ff (q5 @cap-followup-2026-09-10; q3/q4/q7/q8/q9/q10 @row-6 2026-09-12) → now, shown=/total=" +echo " ledger: the ten reference queries (this repo, full legend) — base bytes @8eb669ff (q5 @cap-followup-2026-09-10) → now, shown=/total=" i=0 for q in "${REPO_Q[@]}"; do base="${REPO_BASE[$i]}"; i=$(( i + 1 )) @@ -174,16 +160,28 @@ for q in "${REPO_Q[@]}"; do fi done # nine fixture bundles measured on the pre-fix binary (git-less copies; d5ac29a7) -FX_BASE="fixture|geometry area of a shape|2895 -fixture|call a native function from python|3067 -fixture|parse the config and load it|3277 -ffifix|geometry area of a shape|2050 -ffifix|call a native function from python|3068 +# SEVEN RE-PINNED 2026-09-12 (row 6: sc= short ids, route= as a code, merged rows — the q5 precedent, a TOOL +# change crossing 4%, so the base follows the output; the two under 4% stay at d5ac29a7). The fixtures are frozen, +# git-less copies, so the whole delta is the tool's: +84…+142 B per bundle = the 123 B sc=/route= reading +# (graphlegend.h kForIdRouteLegend) + the compact bundle's merged-callee clause (~70 B, "l= comma-joins the lines +# of same-named callees, shown= counts callees") - the route prose the code replaced (~80 B) - the id= bytes the +# few scoped rows no longer repeat. Measured main @1cf3086e -> this lane's build, each on its own copy: +# fixture geometry area of a shape 2,895 -> 3,037 (+4.91%) call a native function from python 3,067 -> 3,213 (+4.76%) +# fixture parse the config and load it 3,277 -> 3,411 (+4.09%) +# ffifix geometry area of a shape 2,050 -> 2,188 (+6.73%) call a native function from python 3,068 -> 3,202 (+4.37%) +# hostilefix geometry area of a shape 2,775 -> 2,889 (+4.11%) call a native function from python 2,074 -> 2,212 (+6.65%) +# The ten reference queries on this repo stay at their 8eb669ff bases: the same change measures +0.8…+3.9% there +# (q8 +3.93%, q10 +3.86%), inside the ratchet, because a 10 KB bundle absorbs the reading a 2 KB one cannot. +FX_BASE="fixture|geometry area of a shape|3037 +fixture|call a native function from python|3213 +fixture|parse the config and load it|3411 +ffifix|geometry area of a shape|2188 +ffifix|call a native function from python|3202 ffifix|parse the config and load it|3506 -hostilefix|geometry area of a shape|2775 -hostilefix|call a native function from python|2074 +hostilefix|geometry area of a shape|2889 +hostilefix|call a native function from python|2212 hostilefix|parse the config and load it|2928" -echo " ledger: nine fixture bundles — base bytes @d5ac29a7 → now" +echo " ledger: nine fixture bundles — base bytes @d5ac29a7 (seven @row-6 2026-09-12) → now" while IFS='|' read -r fx q base; do now="$( cd "$TMP" && "$BIN" "$fx" --for="$q" 2>/dev/null | wc -c | tr -d ' ' )" pct="$( python3 -c "print( '%+.2f' % ( ( $now - $base ) * 100.0 / $base ) )" )" diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 256591997..dd58072a5 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -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 2a1caf5519f66891ba082520f2342fb1a26678043cccebe20ac0d8b2b1e0d841 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 e4838d9927fdbe7bb49a769e803be31fd7eaeb8cedbd801a5340226c9fa1e928 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index 126ab4fc6..ca5b52a1b 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Sun, 13 Sep 2026 10:38:31 -0400 Subject: [PATCH 08/22] test(sc): three more gates located a fixture symbol by the id= spelling the map no longer prints Row 6 replaced the path-repeating id= on scoped symbol rows with sc=, and three gates still asserted presence with a grep for the old canonical spelling. Two of them were presence GUARDS -- the arms that exist so the gate cannot pass while observing nothing -- so they failed loudly on CI at 76f5747a rather than going inert, which is the guard working: chainguardcheck 16 fixture symbols + the FIX2 cross-language guard, keyed on '::App::goThis"' and its fifteen siblings -> 'n="goThis" sc="App"'. mdsectioncheck the markdown heading HIERARCHY arms, keyed on the composed id '::Orientation Guide::Cache Warm Path"'. A heading row's sc= is the scope CHAIN, so "scoped under X" is now "sc= ends with X" (regex, anchored on the row's own n=) -- the assertion is the same fact, read off the new spelling. packcallersharecheck arm (4b)'s tight --token-budget: the ranking section lost the repeated path, so the callers quota moved with it and 1255 fell out of the one-row window. Re-swept 1200..1340 (step 5, step 2 at the edges): section omitted <=1215, shown="1" on 1220..1250, shown="2" from 1252, fully fit from 1290. Re-pinned to 1235, mid-window. The PROPERTY the arm asserts -- one row admitted, two withheld, capped="1" -- is unchanged. Red/green: all three FAIL on CI at 76f5747a and ALL PASS here. Co-Authored-By: Claude Opus 5 (1M context) --- test/chainguardcheck.sh | 11 ++++++----- test/mdsectioncheck.sh | 8 +++++--- test/packcallersharecheck.sh | 7 ++++++- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/test/chainguardcheck.sh b/test/chainguardcheck.sh index 69684aad3..682e5f0c1 100755 --- a/test/chainguardcheck.sh +++ b/test/chainguardcheck.sh @@ -99,10 +99,11 @@ 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) ── -for want in '::Pool::run"' '::Cfg::enable"' '::EDecoy::enable"' '::Opts::tick"' '::Opts::ping"' \ - '::App::run"' '::App::tick"' '::App::goThis"' '::App::goVar"' '::App::goLoc"' \ - '::App::goShadow"' '::App::goVarShadow"' '::App::goBare"' '::App::goOne"' \ - '::App::deepPin"' '::App::deepShadow"'; do +# row 6 (2026-09-12): a scoped row prints n= then sc= (the short id); the canonical id composes as ::sc::n +for want in 'n="run" sc="Pool"' 'n="enable" sc="Cfg"' 'n="enable" sc="EDecoy"' 'n="tick" sc="Opts"' 'n="ping" sc="Opts"' \ + 'n="run" sc="App"' 'n="tick" sc="App"' 'n="goThis" sc="App"' 'n="goVar" sc="App"' 'n="goLoc" sc="App"' \ + 'n="goShadow" sc="App"' 'n="goVarShadow" sc="App"' 'n="goBare" sc="App"' 'n="goOne" sc="App"' \ + 'n="deepPin" sc="App"' 'n="deepShadow" sc="App"'; 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" @@ -203,7 +204,7 @@ EDG="$( printf '%s\n' "$MAP" | grep -o 'edges=[0-9]*' | head -1 )" # ── (j) cross-language stability (FIX2): Python/TS chained-call edges are byte-stable ───────────────── MAP2="$( "$BIN" "$FIX2" --no-cache 2>/dev/null | tr '>' '\n' )" -printf '%s\n' "$MAP2" | grep -qF '::PApp::go"' || no "(j) presence guard: PApp.go not indexed in FIX2" +printf '%s\n' "$MAP2" | grep -qF 'n="go" sc="PApp"' || no "(j) presence guard: PApp.go not indexed in FIX2" GO2="$( "$BIN" "$FIX2" --callees=go --no-cache 2>/dev/null | grep -o '' | tr '/' '\n' )" ( printf '%s\n' "$GO2" | grep -q 'p.py:2"' ) && ( printf '%s\n' "$GO2" | grep -q 'p.py:6"' ) \ && ok "(j-py) PApp.go() self.pool.acquire() keeps its COMPLETE split (Python shape capture changes no edge)" \ 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/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" \ From 5adfda72431b02d8a1807e862b3ecda7be4fc9dd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 10:39:20 -0400 Subject: [PATCH 09/22] fix(for): the ceiling ladder's free drop was gated by the overshoot allowance, and the route= code shipped with no reading anywhere Two defects on the same header, both found by CI at 76f5747a. 1. RUNG ZERO FIRED ON THE ALLOWANCE, NOT ON THE CEILING. Under an explicit --token-budget, --for prices its header against the delivered-byte allowance (budget x 2.36 x 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). kCeilingFirstEntryTolerance exists for the RESIDUAL a lens cannot trim -- a first signature is not divisible -- but it also gated that first, free drop, so a document 1..15% over its budget that still carried all three clauses shipped over_ceiling="1" with every one of them riding. On CI: fornotesbudgetcheck's 1640 rung measured est_tokens=1755 (+8%) and forrootlegendcheck's arm2 800 rung 831. The free drop is now tried against the number the ROOT PROMISES (budget x 2.36) and only what remains after it is judged by the tolerance; same fit test otherwise. The 1640 rung now reads 1402 with its seven rows intact and the dropped-clause note present, the 800 rung 787. No tolerance moved and no ceiling moved: the rung that costs no unique information simply runs first. estchargecheck's late-label sweep control is re-anchored on that measurement. The control exists to prove the sweep CROSSES the band the defect lives in (a root saying over_ceiling="1" while the document still fits the allowance); with the free drop running earlier that residual band sits at 780..810 on this corpus, not inside 1200..1500, so the arm swept 700..3300 step 10 on the new binary and starts at 760. Left at 1200 it would have been inert, which the gate's own comment forbids. 2. route= WAS A CODE WITH NO READING. Row 6 turned route= from prose into a code, and the reading was then trimmed to the sc= rule alone to buy ceiling headroom -- which left an agent holding route="subtoken+body:declined" with nothing in the document to read it by. legendcoveragecheck (A) named it: two NEW undefined first-screen attributes (for-auto | ctx@route, for-budgeted | ctx@route). The fix is the legend, not the baseline: test/legendcoverage_baseline.txt is a ratchet its own header says may only be edited DOWNWARD, and an upward edit there would have recorded this lane's own regression as accepted debt. The reading is now two clauses, one spelling shared by both dialects (graphlegend.h kForIdRouteLegend 29 B, kForRouteCodeLegend 54 B; verbs_for.h's kForCompactLegendRoute is now an alias of the second, so a code cannot acquire two readings). sc= rides every answer, because every scoped row carries sc=. The route= code rides only the answers whose root carries route= -- forRouteAttrPresent, read off the BUILT root open rather than re-derived, on the CLI lens and the MCP for twin alike. Both stay ceiling-droppable and both stay exempt from the signature-trim charge, with the emitted bytes (not the constant's) subtracted from the sig ledger in each dialect. The dropped-clause note names sc=/route= again. The fuller reading of each code -- what :broad and :declined(word;carriers,defs) weigh, where the anchors are -- still lives once in the help text's no-route entry. Measured. The 83 B of reading is absorbed by every ceiling rung (forbudgetmonotoncheck #1/#5, fornotesbudgetcheck, forrootlegendcheck, compactlegendcheck, fillordercheck all ALL PASS, which is what defect 1 bought). It crosses forrankordercheck's 4% ratchet on the two SMALLEST frozen-fixture bundles only, where 83 B is 4.7%: ffifix/'geometry area of a shape' 2050 -> 2148 (+4.78%), hostilefix/'call a native function from python' 2074 -> 2172 (+4.73%). Those two bases follow the output under the gate's own q5 precedent (a TOOL change crossing 4% on a git-less frozen copy, so the whole delta is the tool's); the other seven fixtures and all ten repository queries stay at their registered bases inside the ratchet. Goldens: docdemotegolden_for.xml 5,383 -> 5,437 B (est_tokens 2153 -> 2175), ONE identified change -- the route clause, which this conceptual query's root carries. Verified before re-pinning: with that clause and est_tokens= normalised out, live and previous goldens are byte-identical. docdemotegolden_noroute.xml is the control and did not move: no route=, no clause. Red/green: legendcoveragecheck (A) reds with 2 new lines against the previous build and ALL PASS here; fornotesbudgetcheck/forrootlegendcheck/estchargecheck red on CI at 76f5747a, ALL PASS here. Co-Authored-By: Claude Opus 5 (1M context) --- src/graphlegend.h | 26 +++++++++++++----- src/mcpverbs.h | 11 ++++++-- src/verbs_for.h | 46 ++++++++++++++++++++++++++------ test/anchorfix/golden_for.xml | 2 +- test/docdemotecheck.sh | 7 +++++ test/docdemotegolden_for.xml | 2 +- test/docdemotegolden_noroute.xml | 2 +- test/estchargecheck.sh | 9 +++++-- test/forrankordercheck.sh | 36 +++++++++++-------------- test/routefix/golden_for.xml | 2 +- 10 files changed, 100 insertions(+), 43 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 3bd23ca23..95d4c53f7 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -283,14 +283,26 @@ inline constexpr const char* kForRootRelAtLegendShort = // lens this clause is CEILING-DROPPABLE (verbs_for.h rung zero, with the confidence and tail clauses): the // attributes stay on every rung, only the reading goes, and kForLegendDroppedNote names it. No "--" anywhere: // it rides inside an XML comment, where a double hyphen is ill-formed (G4). -// TERSE ON PURPOSE, and measured twice. This clause rides EVERY default --for answer; a 259 B first spelling -// (2026-09-12, the same day) grew a 2.9 KB fixture bundle by 10% and tripped forrankordercheck's 4% ratchet on -// three fixtures and six reference queries. What the reader NEEDS beside the row is the composition rule for -// sc= and the vocabulary of the route= code; the reading of each code (what :broad and :declined mean, where -// the anchors are) lives once in the help text's --no-route entry, the next=/help precedent verbs_for.h records. -// No "--" in the clause itself: it rides inside an XML comment, where a double hyphen is ill-formed (G4). +// TERSE ON PURPOSE, and measured four times. These clauses ride EVERY default --for answer, so their bytes are +// bounded by what the same change SAVED on that header: the route= code replaces 84 B of prose on a conceptual +// route and 32 B on a name-exact one. A 259 B first spelling (2026-09-12) grew a 2.9 KB fixture bundle by 10% +// and tripped forrankordercheck's 4% ratchet; a 123 B second spelling still left the header 39 B heavier than +// before on a conceptual route and tipped two rungs that sat at zero slack on main (forrootlegendcheck arm2, +// est_tokens 798 of 800; fornotesbudgetcheck 1640) — the ladder's first-entry tolerance let a document ship +// over_ceiling="1" without reaching rung zero, so a droppable clause was no protection at those rungs (fixed +// in verbs_for.h: rung zero now fires on the EXACT ceiling). +// SPLIT IN TWO, present-only, 2026-09-13. The sc= rule rides every answer, because every scoped row carries +// sc=. The route= code rides only the answers whose root carries route=, exactly as the compact dialect's +// kForCompactLegendRoute does — and it RIDES, rather than living only in the help text, because a code with no +// reading anywhere in the document is an undefined first-screen attribute: dropping it opened two new +// legendcoverage_baseline lines (for-auto, for-budgeted | ctx@route) and that file may only be edited DOWNWARD. +// The fuller reading of each code (what :broad and :declined(word;carriers,defs) weigh, where the anchors are) +// still lives once in the help text's no-route entry, the next=/help precedent verbs_for.h records. +// 29 B + 54 B. No "--" in either clause: they ride inside an XML comment, where a double hyphen is ill-formed (G4). inline constexpr std::string_view kForIdRouteLegend = - "; sc=scope (full id p::sc::n); route= name-exact(X)|subtoken+body[:broad|:declined(w;carriers,defs)], read in the help text"; + "; sc=scope (full id p::sc::n)"; +inline constexpr std::string_view kForRouteCodeLegend = + "; route= name-exact(X)|subtoken+body[:broad|:declined]"; inline const char* forRootRelPathsLegendShort( bool rootOn, bool atOn = false ) noexcept { diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 12312f526..8b469f62f 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1791,10 +1791,16 @@ 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; std::string headerStr = rootOpenStr + "frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()"$TMP/noroute.xml" 2>/dev/null "$BIN" docdemotefix --for="$BUGQ" --no-cache >"$TMP/bugfor.xml" 2>/dev/null "$BIN" docdemotefix --for="$TRACEQ" --no-cache >"$TMP/tracefor.xml" 2>/dev/null +# RE-PIN 2026-09-13 (PR #215, row 6): docdemotegolden_for.xml 5,383 -> 5,437 B (+54 B, est_tokens "2153" +# -> "2175"). ONE identified change: the route= code's reading (graphlegend.h kForRouteCodeLegend) now rides +# the default dialect too, present-only, beside the attribute it defines — this conceptual query routes +# subtoken+body, so its root carries route= and the clause appears. Verified before re-pinning: with that one +# clause and est_tokens= normalised out, the live document and the previous golden are byte-identical — no +# ranking, demotion or route byte moved. The noroute golden is UNCHANGED, which is the control: with no +# route= there is no clause, and (h) proves it. # RE-PIN 2026-09-10 (cap-disclosure lane, fix 2): docdemotegolden_for.xml 5505 -> 5517 B (est_tokens # "2202" -> "2207") and docdemotegolden_noroute.xml 9556 -> 9568 B (est_tokens "3386" -> "3391"). ONE # identified change, +12 B on each = FOUR three-byte U+2026 markers: cleanSig's 240-byte cap diff --git a/test/docdemotegolden_for.xml b/test/docdemotegolden_for.xml index 8a9c535ae..627389051 100644 --- a/test/docdemotegolden_for.xml +++ b/test/docdemotegolden_for.xml @@ -1 +1 @@ -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 4e1851652..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 ): N at 2.50 B/tok, bytes <= 2.714 B/tok). No such budget = inert, re-anchor. diff --git a/test/forrankordercheck.sh b/test/forrankordercheck.sh index 33f1fd965..d713f9a39 100755 --- a/test/forrankordercheck.sh +++ b/test/forrankordercheck.sh @@ -160,28 +160,24 @@ for q in "${REPO_Q[@]}"; do fi done # nine fixture bundles measured on the pre-fix binary (git-less copies; d5ac29a7) -# SEVEN RE-PINNED 2026-09-12 (row 6: sc= short ids, route= as a code, merged rows — the q5 precedent, a TOOL -# change crossing 4%, so the base follows the output; the two under 4% stay at d5ac29a7). The fixtures are frozen, -# git-less copies, so the whole delta is the tool's: +84…+142 B per bundle = the 123 B sc=/route= reading -# (graphlegend.h kForIdRouteLegend) + the compact bundle's merged-callee clause (~70 B, "l= comma-joins the lines -# of same-named callees, shown= counts callees") - the route prose the code replaced (~80 B) - the id= bytes the -# few scoped rows no longer repeat. Measured main @1cf3086e -> this lane's build, each on its own copy: -# fixture geometry area of a shape 2,895 -> 3,037 (+4.91%) call a native function from python 3,067 -> 3,213 (+4.76%) -# fixture parse the config and load it 3,277 -> 3,411 (+4.09%) -# ffifix geometry area of a shape 2,050 -> 2,188 (+6.73%) call a native function from python 3,068 -> 3,202 (+4.37%) -# hostilefix geometry area of a shape 2,775 -> 2,889 (+4.11%) call a native function from python 2,074 -> 2,212 (+6.65%) -# The ten reference queries on this repo stay at their 8eb669ff bases: the same change measures +0.8…+3.9% there -# (q8 +3.93%, q10 +3.86%), inside the ratchet, because a 10 KB bundle absorbs the reading a 2 KB one cannot. -FX_BASE="fixture|geometry area of a shape|3037 -fixture|call a native function from python|3213 -fixture|parse the config and load it|3411 -ffifix|geometry area of a shape|2188 -ffifix|call a native function from python|3202 +# TWO RE-PINNED 2026-09-13 (row 6: sc= short ids, route= as a code, and the route= code's present-only reading — +# the q5 precedent, a TOOL change crossing 4%, so the base follows the output; the seven under 4% stay at +# d5ac29a7). The fixtures are frozen, git-less copies, so the whole delta is the tool's: the 29 B sc= composition +# rule + the 54 B route= code reading (graphlegend.h) + the compact bundle's merged-callee clause, minus the +# route prose the code replaced and the id= bytes the scoped rows no longer repeat. A 2 KB bundle cannot absorb +# 82 B of reading the way the 10 KB repository bundles below do — those measure +0.8…+3.9%, inside the ratchet. +# Measured on this lane's build, each on its own copy: ffifix/geometry 2050 -> 2148 (+4.78%), +# hostilefix/'call a native function from python' 2074 -> 2172 (+4.73%). +FX_BASE="fixture|geometry area of a shape|2895 +fixture|call a native function from python|3067 +fixture|parse the config and load it|3277 +ffifix|geometry area of a shape|2148 +ffifix|call a native function from python|3068 ffifix|parse the config and load it|3506 -hostilefix|geometry area of a shape|2889 -hostilefix|call a native function from python|2212 +hostilefix|geometry area of a shape|2775 +hostilefix|call a native function from python|2172 hostilefix|parse the config and load it|2928" -echo " ledger: nine fixture bundles — base bytes @d5ac29a7 (seven @row-6 2026-09-12) → now" +echo " ledger: nine fixture bundles — base bytes @d5ac29a7 (two @row-6 2026-09-13) → now" while IFS='|' read -r fx q base; do now="$( cd "$TMP" && "$BIN" "$fx" --for="$q" 2>/dev/null | wc -c | tr -d ' ' )" pct="$( python3 -c "print( '%+.2f' % ( ( $now - $base ) * 100.0 / $base ) )" )" diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index ca5b52a1b..09e90e4f2 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Sun, 13 Sep 2026 10:39:37 -0400 Subject: [PATCH 10/22] test(est): the --expand token gate measured its truth with a byte proxy that had drifted 29% off the tokenizer CI at 76f5747a: "--expand est_tokens (26812) NOT within 15% of byte/2.85 proxy (31697)". The gate prices `src --expand=buildGraph --top-k=200` against a real o200k count when tiktoken is importable and against bytes/2.85 when it is not; CI has no tiktoken, so the proxy is what CI judges by. The estimate is not what drifted. Measured on this exact probe with a real o200k_base encoder: 90,338 B / 24,593 real tokens = 3.673 B/tok, and the tool's est_tokens reads 26,812 -- 9.0% high, inside the 15% band on the instrument the proxy is a stand-in FOR. The proxy read the same document as 31,697 tokens, 29% above the real count, and then reported the estimate as 15.4% "under" a truth that was itself wrong. The sc= rows made the document leaner (fewer repeated path bytes, so fewer bytes per token of content) and pushed an already-marginal proxy over the band it was drifting toward. Re-calibrated to 3.40 from the SAME instrument the 2.85 came from: 3.40 keeps the conservative posture 2.85 had against its own 3.08 measurement (about 7% under the measured rate), so the proxy still under-states the truth rather than flattering the estimate. The 15% band is unchanged and no charge moved. When tiktoken is installed the real path runs and this constant is never consulted. Red/green: red on CI at 76f5747a (proxy path); ALL PASS here on the proxy path (python3 without tiktoken) and on the real path (a scratch venv with tiktoken: |26812 - 24593| = 2,219 against a 3,689 limit). Co-Authored-By: Claude Opus 5 (1M context) --- test/expandtokencheck.sh | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/test/expandtokencheck.sh b/test/expandtokencheck.sh index aa754a295..d1cd6dd7d 100755 --- a/test/expandtokencheck.sh +++ b/test/expandtokencheck.sh @@ -56,15 +56,23 @@ EXP_EST="$( est src --expand=buildGraph --top-k=200 )" # blended byte proxy — an --expand payload is body-HEAVY, and code bodies tokenize LEANER (~3.8 B/tok: # whitespace/braces merge) than the map's signature markup (~2.5), so the whole-output blends to ~2.85 # B/tok (MEASURED: 59.5KB → 19.3K real tokens = 3.08; 2.85 is a conservative floor). |est - truth| must -# be <= 15% of truth. Integer math on the byte path. ───────────────────────────────────────────────── +# be <= 15% of truth. Integer math on the byte path. +# RE-CALIBRATED 2026-09-13 (PR #215, sc= on the map rows): the proxy is 3.40, from the SAME instrument the +# 2.85 came from — real o200k counts of this exact probe (`src --expand=buildGraph --top-k=200`) on the two +# binaries either side of the change: 94,702 B / 25,908 real tokens = 3.66 B/tok before, 90,338 B / 24,593 +# = 3.67 after. The estimate tracks the real count on both (est/real 1.103 before, 1.090 after — the leaner +# row made it BETTER), and the 2.85 floor no longer tracks the bytes: it read the new output as 31,697 tokens +# against 24,593 real, and put the estimate 15.4% "under" a truth that was 29% too high. 3.40 keeps the same +# conservative posture 2.85 had against its own 3.08 (about 7% under the measured rate); the 15% band is +# unchanged. When tiktoken is installed the real path runs and this constant is not consulted. ──────────── FULL_BYTES="$( "$BIN" src --expand=buildGraph --top-k=200 --no-cache 2>/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 )) From 0f238011cff830ce4b3d1faed8fdf876fb4910b1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 10:39:55 -0400 Subject: [PATCH 11/22] feat(agent): every agent surface asked for the compact legend and none of them said how to get the full one back Owner question, 2026-09-13: the previous commit put --legend=compact on 158 skill commands, 9 wrap-blurb commands, 26 --help-task routes and 2 tool-call routes, and nothing told the agent reading those commands when or how to ask for the longer answer. An agent that meets a term it does not recognise, a floor it needs explained, or a map a human is going to read now has one sentence telling it to add --legend=full -- one sentence per surface, no more, because a rule repeated seventeen times is a rule nobody reads. Four surfaces: src/wrap.h (wrapUseWhenBlurbLines) -- one line under the trust notes. The two trust-note lines were rewrapped into one in the same edit; the 10-20 line band the blurb is pinned to is unchanged. skills/ripwire-router/SKILL.md -- ONE new section in the router's shared conventions, not seventeen skill bodies. No frontmatter, description, stop rule or boundary moved (skilldescbudgetcheck, skillroutingjudgedcheck). hooks/ripwire-claude-route.sh, ripwire-claude-toolroute.sh, ripwire-codex-route.sh -- a parenthetical inside the additionalContext sentence the hook injects: "(add --legend=full if a definition is unclear)". src/mcprefusal.h -- the `legend` argument's own description now says what the non-default value DOES: "full (restores the full legend)". +32 B on each of the 17 tools that declare the field. mcpmanifestcheck's manifest ceiling moves 42,200 -> 42,700 B, measured 42,636 on this tree. That gate's registered rule is that the ceiling moves for a declared argument's OBLIGED description and not for prose; this is prose on a declared argument, and the owner asked for exactly this clause (2026-09-13) and authorized the pin. That authority and this attribution are the whole justification. Headroom after: 64 B. Gate, red first: wrapverbscheck arm 8, five rows -- the wrap blurb, the router skill, the three route hooks, and a live tools/list over --mcp. Against the previous build all five FAIL; here ALL PASS. Also in this commit: the CHANGELOG entry for the lane (the `--VERB` placeholder in the row-6 paragraph was a literal deckcheck reads as a flag -- it scans prose for --flag tokens and found one that is in no --help and no allowlist; written out as "verb commands"). Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 53 +++++++++++++++++++++++-------- hooks/ripwire-claude-route.sh | 2 +- hooks/ripwire-claude-toolroute.sh | 2 +- hooks/ripwire-codex-route.sh | 2 +- skills/ripwire-router/SKILL.md | 4 +++ src/mcprefusal.h | 2 +- src/wrap.h | 4 +-- test/mcpmanifestcheck.sh | 8 ++++- test/wrapverbscheck.sh | 32 +++++++++++++++++++ 9 files changed, 89 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 04d10dde9..0cf50cc32 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,13 +24,18 @@ itself tells repeated callers to leave: most of a small `--callers`/`--uses`/`-- 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 +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`. +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 @@ -90,21 +95,43 @@ same 185 rows), `test/cppqualfix` 2,935 → 2,781 B, `test/nestedqualfix` 2,045 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, not bytes: three conceptual and name-exact tasks on this -tree served 25 → 28, 21 → 24 and 3 → 3 signature rows at 10,042 → 10,142, 10,256 → 10,180 and -5,971 → 5,886 B; the MCP `for` twin on the same tasks 8,752 → 9,008, 8,882 → 9,001 and 1,965 → 2,025 B -with 26 → 31, 23 → 27 and 1 → 1 rows. The one new legend clause — the `sc=` composition rule and the -`route=` code vocabulary, 123 B, ceiling-droppable with the confidence clause and exempt from the -signature-trim charge like every other disclosure — keeps the readings of the codes in `--help`'s -`--no-route` entry rather than on every answer: a 259 B first spelling grew a 2.9 KB fixture bundle by -10% and tripped `test/forrankordercheck.sh`'s 4% ratchet. The `--json` +tree served 25 → 28, 21 → 24 and 3 → 3 signature rows at 10,042 → 10,048, 10,256 → 10,086 and +5,971 → 5,792 B; the MCP `for` twin on the same tasks 8,752 → 8,914, 8,882 → 8,907 and 1,965 → 1,931 B. +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 on the two smallest fixture +bundles only, which re-pin under the gate's own q5 precedent. The `--json` twins mirror the attribute (`"sc"`), so `mcpattrparity` holds without a rename. Pins moved with the bytes: 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) and the ten-verb loop 4,900 → 5,000 B, all for the one new whole-document `sc=` reading; the -`test/fixture` map's `est_tokens` 884 → 894; seven of `test/forrankordercheck.sh`'s nine frozen-fixture -bundle bases follow the output (+84…+142 B each, the whole delta the tool's, the ten repository queries -inside the ratchet at +0.8…+3.9%); five goldens regenerated for the row shape; the printf-parity manifest -re-pinned for the labels the rows and the help text move. +`test/fixture` map's `est_tokens` 884 → 894; two of `test/forrankordercheck.sh`'s nine frozen-fixture bundle +bases (`ffifix` 2,050 → 2,148, `hostilefix` 2,074 → 2,172 — the two smallest, where 83 B of reading is 4.7%), +its other seven and its ten repository queries staying at their registered bases inside the ratchet; five +goldens regenerated for the row shape; the printf-parity manifest re-pinned for the labels the rows and the +help text move. + +### 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: the same +1640 rung reads 1402 with its seven rows intact and the dropped-clause note present, the 800 rung 787. +`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). ### Added — Elixir module and arity resolution (parser version 95) diff --git a/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index ef04060ad..5934f16aa 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -238,6 +238,6 @@ fi # printf, not an inline \n: inside double quotes the shell keeps \n as two literal characters, and the # injected context then carries a visible backslash-n instead of a line break. -context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation before tool selection. Prefer it when it answers the task; continue beyond it when implementation or verification still needs more evidence.' "$route" )" +context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation before tool selection. Prefer it when it answers the task (add --legend=full if a definition is unclear); continue beyond it when implementation or verification still needs more evidence.' "$route" )" jq -cn --arg context "$context" \ '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$context}}' 2>/dev/null || exit 0 diff --git a/hooks/ripwire-claude-toolroute.sh b/hooks/ripwire-claude-toolroute.sh index d462b2f5c..10a7056ac 100755 --- a/hooks/ripwire-claude-toolroute.sh +++ b/hooks/ripwire-claude-toolroute.sh @@ -406,7 +406,7 @@ case "$recommended" in esac [ -n "$runCmd" ] || exit 0 -context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation from the shape of this tool call, before it ran. Prefer it when it answers the need; continue with the original call when more evidence is still required.' "$runCmd" )" +context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation from the shape of this tool call, before it ran. Prefer it when it answers the need (add --legend=full if a definition is unclear); continue with the original call when more evidence is still required.' "$runCmd" )" jq -cn --arg context "$context" \ '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"allow",additionalContext:$context}}' \ 2>/dev/null || exit 0 diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 78f35a67a..5538b1b11 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -136,6 +136,6 @@ fi [ "$status" = recommend ] || exit 0 # printf, not an inline \n: inside double quotes the shell keeps \n as two literal characters, and the # injected context then carries a visible backslash-n instead of a line break. -context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation before tool selection. Prefer it when it answers the task; continue beyond it when implementation or verification still needs more evidence.' "$route" )" +context="$( printf '%s\n%s' 'Ripwire produced a confidence-gated CLI recommendation before tool selection. Prefer it when it answers the task (add --legend=full if a definition is unclear); continue beyond it when implementation or verification still needs more evidence.' "$route" )" jq -cn --arg context "$context" \ '{hookSpecificOutput:{hookEventName:"UserPromptSubmit",additionalContext:$context}}' 2>/dev/null || exit 0 diff --git a/skills/ripwire-router/SKILL.md b/skills/ripwire-router/SKILL.md index 557c9da67..e32302b92 100644 --- a/skills/ripwire-router/SKILL.md +++ b/skills/ripwire-router/SKILL.md @@ -82,6 +82,10 @@ grew 32K → 256K, LongCodeBench). If a `ripwire wrap` primer or the opt-in `ski nudge is installed, these same substitutions arrive without anyone loading this file — that is the point: a rule an agent must remember to look up is a rule that loses to a habit. +## The legend: compact by default, full when a definition needs its reasoning + +Every command these skills spell asks for the compact legend (`--legend=compact`: terse definitions of only the attributes the answer carries). Add `--legend=full` when a definition's reasoning is needed — a term you do not recognise, a floor or cap you need explained, or a map a human will read. `--for` is the exception: its default legend is already the one to read first, and its compact legend is its own. + ## Cross-cutting disciplines (fire ALONGSIDE a moment skill, not instead) - **ripwire-orient / `map-before-you-read.md`** — the map-before-you-read token+accuracy discipline for *any* read, at *any* moment (a companion file since 2026-09-07; it was the standalone `efficient` skill). diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 923d579e0..7de5b6ae2 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/wrap.h b/src/wrap.h index 96f4236c1..ce9281e0a 100644 --- a/src/wrap.h +++ b/src/wrap.h @@ -252,8 +252,8 @@ inline std::vector wrapUseWhenBlurbLines() " (or insert-before/after); the receipt carries region, blob_sha, edit_check, tests_to_run + ONE next= — no re-read after it; `--edit-check=SYM --legend=compact` is for a contract question WITHOUT an edit in hand.", "- Before writing a new fn/class/helper: `--exemplar=\"\" --legend=compact` — duplicates are born on small tasks.", "- Before calling work done: `--quality-delta --legend=compact` (what you made worse), then `--test-gate --legend=compact`.", - "- Trust notes: counts marked counts_floor are floors, not totals; a zero means \"none", - " found\", never \"none exists\".", + "- Trust notes: counts marked counts_floor are floors, not totals; a zero means \"none found\", never \"none exists\".", + "- The commands above ask for the compact legend (terse definitions of only the attributes present); 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.", "Defaults to break (less context is measurably MORE accurate, not just cheaper — code-repair", "accuracy fell 29% -> 3% as context grew 32K -> 256K tokens, LongCodeBench):", "- Do NOT open a file you have not located first: rank with `--for`/`--grep`, then read what it names.", diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 1664d4874..7b91f5a90 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -202,7 +202,13 @@ tools = json.loads( line )[ "result" ][ "tools" ] # TOTAL 41,220 -> 42,084 B on the merged tree, attributed tool by tool against main's binary # (both lanes had re-anchored alone — 41,650 and 42,000 — and the sum is what ships). # Headroom after this line: 116 B, less than one declared argument, which is rule 5 above working. -CEILING = 42200 +# 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. +CEILING = 42700 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/wrapverbscheck.sh b/test/wrapverbscheck.sh index 655b690a7..903b595b4 100755 --- a/test/wrapverbscheck.sh +++ b/test/wrapverbscheck.sh @@ -339,6 +339,38 @@ 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" From 88355acd0a12431be871da712b09d019f68c0dbf Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 12:57:12 -0400 Subject: [PATCH 12/22] =?UTF-8?q?fix(for,hooks,docs):=20the=20review=20rou?= =?UTF-8?q?nd=20=E2=80=94=20a=20ladder=20priced=20in=20the=20wrong=20unit,?= =?UTF-8?q?=20a=20note=20about=20clauses=20that=20never=20rode,=20and=20a?= =?UTF-8?q?=20shape=20that=20outlived=20its=20own=20output?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ten confirmed findings from the 2026-09-13 review of #215, every one reproduced against this build before it was touched. 1. THE LADDER WAS PRICED IN A UNIT ITS ROOT DOES NOT USE. --for's rung zero tested fit at kMinBytesPerToken (2.36) while est_tokens= and over_ceiling= price at kBytesPerTokenDefault (2.50), so the rung fired on documents 6% INSIDE the budget their own root reports: `test/cppqualfix --for="widget ping make box" --token-budget=1200` printed est_tokens="778", no over_ceiling=, and had dropped all three droppable legend clauses "(ceiling)"; at 1300 the same query kept every clause. rw::ceilingBytes( budgetTokens ) is the one expression for "what does this root promise", beside ceilingAllowanceBytes, and the ladder now takes BOTH: rungs (a)/(b) — as built, and the task echo, a byte-for-byte duplicate of task= — aim at the exact ceiling, while (c) route= and (d) the honest label keep the first-entry tolerance, which exists for a residual a lens cannot trim and not for the free rungs. --pack-task had the identical defect in the identical words and takes the identical pair. Now: est_tokens="1149" at --token-budget=1200, every clause riding. --from-trace passes one ceiling twice (it states its ceiling in bytes and has no rate mismatch), so its climb is byte-identical. The three open-coded `budgetTokens * kMinBytesPerToken` in packtask.h became rw::declaredByteCeiling — the number that lens PUBLISHES as budget_ceiling_bytes. Deliberately not repointed at ceilingBytes: that is a published contract and no defect asks it to move. 2. THE DROPPED-LEGEND NOTE DESCRIBED A DIFFERENT DOCUMENT. Four constants chosen by one coverage= lookup, three of them wrong: the THIN spellings named neither sc= nor route= although rung zero clears that reading on a thin answer too (`--for='mutex and pick overloads' --token-budget=300` served two rows, mentioned sc= nowhere, and disclosed nothing); the default spelling named route= even under --no-route, reporting a cut of a definition that never rode; the compact spellings named sc=, which that dialect defines in an un-droppable clause. ONE assembler, legendDroppedNote( compact, thin, routeWasOn, scWasOn ), builds the note from the four facts that decide what was there to lose. The old comment's argument for constants — "the byte ledgers read their sizes" — was not true of any caller. 3. THE COMPACT RUNG WAS BYTE-NEGATIVE. It removed 110-164 B of clauses and spliced a 161 B note: +51 B net on a route-less answer, so the rung 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 comparison IS the assertion — there is no shape in which the byte-negative branch can be reached. 4. THE MCP FILE PAGE ANSWERED IN A RETIRED SPELLING. It composed `"routed: " + rc.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, and a parity break with the CLI page and with its own default serving. filter.h routeNoteOf() is the one producer; all four sites call it. 5. A GATE ENFORCED A COMMAND THE BINARY REFUSES. skills/ripwire-orient spelled `--zoom --legend=compact --mermaid`, which is refused ("--legend=compact applies to the XML verbs only"), and skilltruthcheck's hand-typed SKILL_COMPACT_VERBS listed zoom, so the gate held the broken line in place. The verb never decided this; the whole command does. The policy arm now RUNS each distinct --legend=compact command the skills spell against an empty directory and reads the refusal — 43 commands, asked rather than listed — and that probe immediately found two more broken lines in ripwire-quality-bar (--quality-ack writes a ledger). All three fixed. taskroute's two flagless routes turn out to be CORRECT (--situ and --edit-plan are refused too), so the finding does not reproduce as stated; the posture is applied ONCE in classify() over result.choices via rw::legendCompactAppliesTo (compactlegend.h, the same non-XML list cli.h refuses on, asked of a command string), and taskroutecheck's new R-LEG arm runs all 34 generated commands against the binary. 6. THE HOOKS AND THE METER DISAGREED ABOUT THE SAME LINE. The observe regex missed every wrapped invocation an agent types (`time ./build/ripwire`, sudo, env, xargs, exec, nohup, `if ripwire`, `{ ripwire`) and still matched `git commit -m "fix; ripwire hook"`; the nudge hook's meter kept a third opinion, and the substitution rate is a ratio of those counts. rw_is_ripwire_call is the shell's own model — walk the words, ask whether any COMMAND-POSITION word basenames to ripwire, step over the wrapper words, `cd DIR`, `rtk proxy` and VAR= prefixes — mirrored byte-identical in the three hooks and asked by the meter's classifier too. routehookcheck O9 diffs the three copies and reads 18 shapes: 13 wrapped or sequenced calls, 5 appearances that run nothing. 7. A GATE REPORTED SUCCESS FOR WORK IT SKIPPED. attrvocabcheck arm 8 matched map rows by `id="PATH::…"`, found none after row 6, left checked at 0 and printed a PASS. Re-keyed to n= within the block that owns it, and it now FAILS on checked == 0. Six rows cross-checked where zero were. 8. A CAP COULD CUT AN ANSWER IT DID NOT NEED TO. A merged callee row was charged name+16 while printing about 4 B, so a block of overloads exhausted its budget early and wrote capped="1" over a listing that would have fit — the cap-makes-the- answer-wrong class. Charged at what it prints. And l= was appended in WALK order, which is RANK order, so one fact had two spellings between queries (l="70,69" / l="69,70"); sorted ascending, row order unchanged. 9. THE REMOVED SHAPE OUTLIVED ITS OUTPUT. --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 — a repetition AND an undefined first-screen attribute. sc= there, one presence rule (serialize.h hasScopeAttr, which the four sc= emitters now share), and the whole-file root states the composition. bench/shotgun/cc_static.py keyed on s.get('id'), which is None on every row 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; keyed on p::sc::n. README's two samples and skills/ripwire-mcp's `[routed: …]` line re-spelled. scroundtripcheck (E2) covers whole-file expand and the candidates export. docs/COMMANDS.md and its capture are the next commit: the capture must be recorded in a ref-clean clone (this machine has 330 local branches and showcase_capture.py publishes branch names). Cheap cleanups the angles named, folded in: ONE sc= presence rule (hasScopeAttr, five readers); ONE "root carries route=" predicate and ONE coverage= predicate; ONE helper returning the sc=/route= clause bytes so the append and the sigs-charge exemption cannot disagree (rw::forIdRouteLegendParts, CLI and MCP); ONE fitsWithin lambda parameterised by ceiling, replacing the two that had drifted on the rate; ONE droppable bit (legendDroppable()/dropDroppableLegend()) replacing four fields moved in lock step behind an always-true guard; kForCompactLegendRows is present-only on sc= like every other clause in that dialect. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 +- bench/shotgun/cc_static.py | 7 +- hooks/ripwire-claude-route.sh | 73 +++++++- hooks/ripwire-codex-route.sh | 73 +++++++- hooks/ripwire-nudge.sh | 72 +++++++- skills/ripwire-mcp/SKILL.md | 2 +- skills/ripwire-orient/SKILL.md | 2 +- skills/ripwire-quality-bar/SKILL.md | 4 +- src/compactlegend.h | 32 ++++ src/filter.h | 18 ++ src/graphlegend.h | 23 +++ src/main.cpp | 11 ++ src/mcpverbs.h | 9 +- src/packtask.h | 13 +- src/serialize.h | 145 ++++++++++++---- src/taskroute.h | 26 +++ src/tracelocus.h | 6 + src/verbs_for.h | 248 ++++++++++++++++++++-------- test/attrvocabcheck.sh | 14 +- test/routehookcheck.sh | 57 +++++++ test/scroundtripcheck.sh | 31 ++++ test/skilltruthcheck.sh | 71 +++++--- test/taskroutecheck.sh | 35 ++++ 23 files changed, 833 insertions(+), 156 deletions(-) diff --git a/README.md b/README.md index b54ccdd85..caa44d8b9 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 --> - - - + + + - + ``` 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/hooks/ripwire-claude-route.sh b/hooks/ripwire-claude-route.sh index 5934f16aa..ef0e82fbd 100755 --- a/hooks/ripwire-claude-route.sh +++ b/hooks/ripwire-claude-route.sh @@ -102,6 +102,68 @@ resolve_arm() # one that carries the recommended verb makes the outcome `adopted`. Two, because a longer window # collects verbs the agent would have reached anyway — that choice is registered, not tuned. # ═══════════════════════════════════════════════════════════════════════════════════════════════════ +# ---- BEGIN MIRRORED BLOCK rw_is_ripwire_call (PR #215 review item 6) ------------------------------------- +# KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh, hooks/ripwire-codex-route.sh and hooks/ripwire-nudge.sh. +# test/routehookcheck.sh extracts the three copies and diffs them, the kIngestParserVerMirror pattern: three +# files answering one question must answer it in one text, or the meter and the hooks disagree about the very +# same command line — which is exactly what happened, and it makes the adoption numbers unreadable. +# +# WHAT THIS REPLACES. A regex that looked for `ripwire` after a separator. It said NO to every WRAPPED +# invocation an agent actually types — `time ./build/ripwire .`, `sudo ripwire`, `env RIPWIRE_BIN=x ripwire`, +# `xargs ripwire`, `exec ripwire`, `nohup ripwire`, `if ripwire … ; then`, `{ ripwire … ; }` — and still said +# YES to `git commit -m "fix; ripwire hook"`, where the word sits inside a quoted string and no ripwire runs. +# Both errors corrupt the same measurement in opposite directions. +# +# WHAT IT DOES. The shell's own model, the one meter_lead in the nudge hook already used: split the line into +# words, walk it, and ask whether any COMMAND-POSITION word is the binary. Command position is the start of the +# line and anything after a separator; the wrapper words below are stepped over because they do not consume the +# command, and `cd DIR`, `rtk proxy` and `VAR=value` prefixes are stepped over with their operand. The word is +# then basename'd, so `./build/ripwire` and `/opt/rw/ripwire` count and `/opt/ripwire/bin/other` does not. +# Unquoted splitting is what makes the quoted-string case come out right: `-m "fix;` and `ripwire` are two +# words, and the second is not in command position because the first did not end a command. +rw_is_ripwire_call() +{ + set -f + # shellcheck disable=SC2086 + set -- $1 + set +f + rw_at_cmd=1 + while [ "$#" -gt 0 ] + do + if [ "$rw_at_cmd" = 1 ] + then + case "$1" in + '&&'|'||'|';'|'&'|'|'|'{'|'('|'!') shift; continue ;; + if|while|until|do|then|else|elif|done|fi|esac) shift; continue ;; + *=*) shift; continue ;; + sudo|command|env|time|nice|nohup|exec|builtin|xargs) shift; continue ;; + cd|pushd) + shift + case "${1:-}" in + ''|'&&'|'||'|';'|'&'|'|') ;; + *) shift ;; + esac + continue ;; + rtk) + shift + if [ "${1:-}" = "proxy" ]; then shift; fi + continue ;; + esac + rw_word="${1##*/}" + if [ "$rw_word" = "ripwire" ]; then return 0; fi + rw_at_cmd=0 + shift + continue + fi + case "$1" in + '&&'|'||'|';'|'&'|'|') rw_at_cmd=1 ;; + esac + shift + done + return 1 +} +# ---- END MIRRORED BLOCK rw_is_ripwire_call --------------------------------------------------------------- + if [ "${1:-}" = "--observe" ]; then meter_home || exit 0 session="$( printf '%s' "$input" | jq -r '.session_id // .conversation_id // empty' 2>/dev/null )" @@ -121,12 +183,11 @@ if [ "${1:-}" = "--observe" ]; then observed="" case "$tool" in Bash) - # 2026-09-12: only the COMMAND WORD counts as a ripwire call — `ripwire`, `./build/ripwire`, any path whose - # basename is ripwire — in command position: at the start, after ; & | ( or $(, past any leading VAR=value - # assignments. A token ending in /ripwire in ARGUMENT position (`cd …/ripwire && git log`, `ls /opt/ripwire`) - # used to count, burn a window slot, and turn a real adoption two commands later into `missed` (the local - # routing analysis's instrument bug). Gate: test/routehookcheck.sh O7 / test/codexpromptroutecheck.sh. - printf '%s' "$command" | grep -Eq '(^|[;&|(]|\$\()[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*([^[:space:]]*/)?ripwire([[:space:]]|$)' || exit 0 + # Only a COMMAND-POSITION word counts as a ripwire call, wrappers and all: rw_is_ripwire_call, the + # block mirrored in the three hooks (see its own comment). A token ending in /ripwire in ARGUMENT + # position (`cd …/ripwire && git log`) does not count and never did burn a window slot since. + # Gate: test/routehookcheck.sh O7/O9 / test/codexpromptroutecheck.sh. + rw_is_ripwire_call "$command" || exit 0 observed="$( printf '%s' "$command" | grep -oE -- '--[a-z0-9-]+' | head -1 )" [ -n "$observed" ] || observed="" ;; diff --git a/hooks/ripwire-codex-route.sh b/hooks/ripwire-codex-route.sh index 5538b1b11..7c4adc3d2 100755 --- a/hooks/ripwire-codex-route.sh +++ b/hooks/ripwire-codex-route.sh @@ -25,6 +25,68 @@ hash_text() printf '%s' "$1" | cksum 2>/dev/null | cut -d' ' -f1 } +# ---- BEGIN MIRRORED BLOCK rw_is_ripwire_call (PR #215 review item 6) ------------------------------------- +# KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh, hooks/ripwire-codex-route.sh and hooks/ripwire-nudge.sh. +# test/routehookcheck.sh extracts the three copies and diffs them, the kIngestParserVerMirror pattern: three +# files answering one question must answer it in one text, or the meter and the hooks disagree about the very +# same command line — which is exactly what happened, and it makes the adoption numbers unreadable. +# +# WHAT THIS REPLACES. A regex that looked for `ripwire` after a separator. It said NO to every WRAPPED +# invocation an agent actually types — `time ./build/ripwire .`, `sudo ripwire`, `env RIPWIRE_BIN=x ripwire`, +# `xargs ripwire`, `exec ripwire`, `nohup ripwire`, `if ripwire … ; then`, `{ ripwire … ; }` — and still said +# YES to `git commit -m "fix; ripwire hook"`, where the word sits inside a quoted string and no ripwire runs. +# Both errors corrupt the same measurement in opposite directions. +# +# WHAT IT DOES. The shell's own model, the one meter_lead in the nudge hook already used: split the line into +# words, walk it, and ask whether any COMMAND-POSITION word is the binary. Command position is the start of the +# line and anything after a separator; the wrapper words below are stepped over because they do not consume the +# command, and `cd DIR`, `rtk proxy` and `VAR=value` prefixes are stepped over with their operand. The word is +# then basename'd, so `./build/ripwire` and `/opt/rw/ripwire` count and `/opt/ripwire/bin/other` does not. +# Unquoted splitting is what makes the quoted-string case come out right: `-m "fix;` and `ripwire` are two +# words, and the second is not in command position because the first did not end a command. +rw_is_ripwire_call() +{ + set -f + # shellcheck disable=SC2086 + set -- $1 + set +f + rw_at_cmd=1 + while [ "$#" -gt 0 ] + do + if [ "$rw_at_cmd" = 1 ] + then + case "$1" in + '&&'|'||'|';'|'&'|'|'|'{'|'('|'!') shift; continue ;; + if|while|until|do|then|else|elif|done|fi|esac) shift; continue ;; + *=*) shift; continue ;; + sudo|command|env|time|nice|nohup|exec|builtin|xargs) shift; continue ;; + cd|pushd) + shift + case "${1:-}" in + ''|'&&'|'||'|';'|'&'|'|') ;; + *) shift ;; + esac + continue ;; + rtk) + shift + if [ "${1:-}" = "proxy" ]; then shift; fi + continue ;; + esac + rw_word="${1##*/}" + if [ "$rw_word" = "ripwire" ]; then return 0; fi + rw_at_cmd=0 + shift + continue + fi + case "$1" in + '&&'|'||'|';'|'&'|'|') rw_at_cmd=1 ;; + esac + shift + done + return 1 +} +# ---- END MIRRORED BLOCK rw_is_ripwire_call --------------------------------------------------------------- + if [ "${1:-}" = "--observe" ]; then meter_home || exit 0 session="$( printf '%s' "$input" | jq -r '.session_id // .conversation_id // empty' 2>/dev/null )" @@ -43,12 +105,11 @@ if [ "${1:-}" = "--observe" ]; then observed="" case "$tool" in Bash) - # 2026-09-12: only the COMMAND WORD counts as a ripwire call — `ripwire`, `./build/ripwire`, any path whose - # basename is ripwire — in command position: at the start, after ; & | ( or $(, past any leading VAR=value - # assignments. A token ending in /ripwire in ARGUMENT position (`cd …/ripwire && git log`, `ls /opt/ripwire`) - # used to count, burn a window slot, and turn a real adoption two commands later into `missed` (the local - # routing analysis's instrument bug). Gate: test/routehookcheck.sh O7 / test/codexpromptroutecheck.sh. - printf '%s' "$command" | grep -Eq '(^|[;&|(]|\$\()[[:space:]]*([A-Za-z_][A-Za-z0-9_]*=[^[:space:]]*[[:space:]]+)*([^[:space:]]*/)?ripwire([[:space:]]|$)' || exit 0 + # Only a COMMAND-POSITION word counts as a ripwire call, wrappers and all: rw_is_ripwire_call, the + # block mirrored in the three hooks (see its own comment). A token ending in /ripwire in ARGUMENT + # position (`cd …/ripwire && git log`) does not count. + # Gate: test/routehookcheck.sh O7/O9 / test/codexpromptroutecheck.sh. + rw_is_ripwire_call "$command" || exit 0 observed="$( printf '%s' "$command" | grep -oE -- '--[a-z0-9-]+' | head -1 )" [ -n "$observed" ] || observed="" ;; diff --git a/hooks/ripwire-nudge.sh b/hooks/ripwire-nudge.sh index 2a29a6a57..b9f1c13fc 100755 --- a/hooks/ripwire-nudge.sh +++ b/hooks/ripwire-nudge.sh @@ -530,6 +530,68 @@ meter_log() meter_w1="" meter_w2="" meter_arg1="" +# ---- BEGIN MIRRORED BLOCK rw_is_ripwire_call (PR #215 review item 6) ------------------------------------- +# KEEP BYTE-IDENTICAL in hooks/ripwire-claude-route.sh, hooks/ripwire-codex-route.sh and hooks/ripwire-nudge.sh. +# test/routehookcheck.sh extracts the three copies and diffs them, the kIngestParserVerMirror pattern: three +# files answering one question must answer it in one text, or the meter and the hooks disagree about the very +# same command line — which is exactly what happened, and it makes the adoption numbers unreadable. +# +# WHAT THIS REPLACES. A regex that looked for `ripwire` after a separator. It said NO to every WRAPPED +# invocation an agent actually types — `time ./build/ripwire .`, `sudo ripwire`, `env RIPWIRE_BIN=x ripwire`, +# `xargs ripwire`, `exec ripwire`, `nohup ripwire`, `if ripwire … ; then`, `{ ripwire … ; }` — and still said +# YES to `git commit -m "fix; ripwire hook"`, where the word sits inside a quoted string and no ripwire runs. +# Both errors corrupt the same measurement in opposite directions. +# +# WHAT IT DOES. The shell's own model, the one meter_lead in the nudge hook already used: split the line into +# words, walk it, and ask whether any COMMAND-POSITION word is the binary. Command position is the start of the +# line and anything after a separator; the wrapper words below are stepped over because they do not consume the +# command, and `cd DIR`, `rtk proxy` and `VAR=value` prefixes are stepped over with their operand. The word is +# then basename'd, so `./build/ripwire` and `/opt/rw/ripwire` count and `/opt/ripwire/bin/other` does not. +# Unquoted splitting is what makes the quoted-string case come out right: `-m "fix;` and `ripwire` are two +# words, and the second is not in command position because the first did not end a command. +rw_is_ripwire_call() +{ + set -f + # shellcheck disable=SC2086 + set -- $1 + set +f + rw_at_cmd=1 + while [ "$#" -gt 0 ] + do + if [ "$rw_at_cmd" = 1 ] + then + case "$1" in + '&&'|'||'|';'|'&'|'|'|'{'|'('|'!') shift; continue ;; + if|while|until|do|then|else|elif|done|fi|esac) shift; continue ;; + *=*) shift; continue ;; + sudo|command|env|time|nice|nohup|exec|builtin|xargs) shift; continue ;; + cd|pushd) + shift + case "${1:-}" in + ''|'&&'|'||'|';'|'&'|'|') ;; + *) shift ;; + esac + continue ;; + rtk) + shift + if [ "${1:-}" = "proxy" ]; then shift; fi + continue ;; + esac + rw_word="${1##*/}" + if [ "$rw_word" = "ripwire" ]; then return 0; fi + rw_at_cmd=0 + shift + continue + fi + case "$1" in + '&&'|'||'|';'|'&'|'|') rw_at_cmd=1 ;; + esac + shift + done + return 1 +} +# ---- END MIRRORED BLOCK rw_is_ripwire_call --------------------------------------------------------------- + meter_lead() { set -f @@ -719,9 +781,15 @@ meter_classify_head() _blead="$meter_w1" _bsub="$meter_w2" mclass="" + # PR #215 review item 6: the ripwire decision is rw_is_ripwire_call's, not a second opinion about the lead + # word. meter_lead stops at the first command word, so `time ./build/ripwire .` classified as `time` here + # while the route hooks (once they were fixed) counted it as a call — one line, two answers, and the + # substitution rate is a ratio of the two counts. One block, mirrored, asked by both. + if rw_is_ripwire_call "$_bc" + then + mclass="ripwire-cli"; return 0 + fi case "$_blead" in - ripwire) - mclass="ripwire-cli"; return 0 ;; grep|egrep|fgrep|zgrep|rg|ag|ack|ack-grep|ugrep) # A COUNT-ONLY or QUIET grep is a POLL, not a search (2026-09-02, from mining the log for # the A/B readout: ~14% of that window's grep-class rows were these). `grep -c Building diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index b234b55aa..d72af6b8f 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -90,7 +90,7 @@ enclosing-chain report) and `@FILE:LINE` in any SYM selector; contract gate: `te | Verb | CLI twin | Ask it for | |---|---|---| | `analyze` | `ripwire ` | the ranked XML map | -| `for` (`task`) | `--for=TASK` | the task lens: signatures + cx/in metrics framed for reuse. **Auto-routes** the ranker — pass a symbol NAME verbatim as `task` to get name-exact retrieval (recall@1 ~99%); a conceptual phrase uses subtoken+body. Header prints `[routed: …]`. | +| `for` (`task`) | `--for=TASK` | the task lens: signatures + cx/in metrics framed for reuse. **Auto-routes** the ranker — pass a symbol NAME verbatim as `task` to get name-exact retrieval (recall@1 ~99%); a conceptual phrase uses subtoken+body. Root carries `route=` as a code (`name-exact(X)` / `subtoken+body[:broad|:declined]`). | | `find_symbol` (`symbol`) | `--callers` + `--callees` | locate a symbol with its callers AND callees in one call — each symbol carries a `handle` | | `find_referencing_symbols` (`symbol`) | `--callers=SYM` | just who references/calls it — also handle-bearing | | `grep` (`pattern`) | `--grep=STR` | parallel literal scan + enclosing symbol + matched line | diff --git a/skills/ripwire-orient/SKILL.md b/skills/ripwire-orient/SKILL.md index 4b0a52473..9c340a5d8 100644 --- a/skills/ripwire-orient/SKILL.md +++ b/skills/ripwire-orient/SKILL.md @@ -125,7 +125,7 @@ Read top-down; a `dir=` that doesn't match its parent's is a cross-cutting conce Trailing `` entries name the high-traffic integration seams *between* top modules — pair with `--seams` to see which ones no test reaches. -**8. Render it** — `ripwire --zoom --legend=compact --mermaid` (or `--mermaid` for the flat module graph): a +**8. Render it** — `ripwire --zoom --mermaid` (or `--mermaid` for the flat module graph): a `flowchart TB`, paste at mermaid.live. For hand-exploring, `ripwire --html[=FILE]` writes a self-contained clickable wiki (module cards → subgraphs → Sourcetrail-style node recentering, no CDN); `--color-by=community|cx|churn|tested` sets its initial node-colour lens (a live selector covers the rest). diff --git a/skills/ripwire-quality-bar/SKILL.md b/skills/ripwire-quality-bar/SKILL.md index 8e6489ddf..8329633fb 100644 --- a/skills/ripwire-quality-bar/SKILL.md +++ b/skills/ripwire-quality-bar/SKILL.md @@ -185,7 +185,7 @@ something always fires.) id, or its **facet**: ```bash - ripwire --quality-delta --legend=compact --ack-only=contract-change --quality-ack="arity change required by " + ripwire --quality-delta --ack-only=contract-change --quality-ack="arity change required by " ``` Prefer the facet over the kind when one exists: `api-surface` also covers the never-gating `new-symbol` @@ -203,7 +203,7 @@ something always fires.) ```bash ripwire --quality-delta --legend=compact --scope=src/render,src/render_gl.h # gate on MY subtree only - ripwire --quality-delta --legend=compact --scope=src/render --quality-ack="deliberate" # …and ack only my rows + ripwire --quality-delta --scope=src/render --quality-ack="deliberate" # …and ack only my rows ``` Rows outside the scope are **still printed**, under an `` element with a do-not-ack diff --git a/src/compactlegend.h b/src/compactlegend.h index 14b794397..5921290ba 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -36,6 +36,38 @@ namespace rw { +// THE SAME QUESTION, ASKED OF A COMMAND STRING (PR #215 review item 5). The list below refuses --legend on a +// parsed Config; the generators — the prompt router's line, the tool-call router, `ripwire wrap`'s paste +// block — hold a command STRING and had no way to ask it, so the posture was applied by editing each string and +// a skill shipped `--zoom --legend=compact --mermaid`, which this very function refuses. One list, two callers. +// +// The flags named here are exactly the `nonXml` arms below, plus --for, which is exempt by POLICY rather than by +// refusal (the binary compacts --for perfectly well; A1-2's decision is that the first call of a session wants +// the full legend, and --for's compact legend is its own dialect). A command that already states a posture is +// left alone, so applying this twice cannot produce two --legend= flags. +// Gate: test/taskroutecheck.sh runs every command the router generates and asserts the binary never refuses it; +// test/skilltruthcheck.sh does the same for every command the skills spell. +inline bool legendCompactAppliesTo( std::string_view command ) +{ + static constexpr std::string_view kNotCompactable[] = { + "--legend=", // already stated + "--for=", // policy exemption, not a refusal + "--situ", "--recall=", "--report", "--mermaid", "--html", "--plan-lanes", "--sarif", "--eval", + "--export", "--note-add=", "--quality-baseline", "--quality-ack", "--index-out=", "--pin-census=", + "--baseline", "--replace-symbol-body", "--insert-before-symbol", "--insert-after-symbol", "--edit-plan=", + "--mcp", "--listen=", + }; + for( std::string_view flag : kNotCompactable ) + { + if( command.find( flag ) != std::string_view::npos ) + { + return false; + } + } + return true; +} + + // One entry per XML root the tool emits. `key` is the schema id stem (ripwire./v1); `purpose` is the reading // of the verb and of the root vocabulary EVERY answer of that root carries. Its bytes count against the verb's // per-verb pin in test/compactlegendcheck.sh, and that pin is measured from the definitions, never the reverse. diff --git a/src/filter.h b/src/filter.h index 886c87ca0..2a24f9b6f 100644 --- a/src/filter.h +++ b/src/filter.h @@ -386,6 +386,24 @@ inline std::string shapeFactorText( int pct ) // The ONE spelling of what happened, appended to the routed reason so it lands in route= (and its JSON // twin) verbatim. Empty when no shape fired — silence means nothing happened, the same convention route= // and over_ceiling already use. +// +// PR #215 review item 4 — and the ONE PRODUCER of the whole route= value is routeNoteOf() below it, because the +// four sites that built this string by hand did not all build the same string. Row 6 made route= a CODE +// (`name-exact(X)`, `subtoken+body[:broad|:declined(…)]`) and dropped the "routed: " prose prefix at three of +// them; the MCP FILE PAGE kept `"routed: " + rc.reason`, so one server, on one query, answered its bundle with +// `route="name-exact(pick)"` and its page with `route="routed: name-exact(pick)"` — a spelling no legend in the +// product defines, and a parity break with both the CLI page and this server's own default serving. A value +// with a vocabulary needs a producer, not four spellings. +inline std::string shapeDemotionNote( const queryshape::Verdict& shape ); + +// `noRoute` is the caller's --no-route / no_route: the router never ran, so there is no route to report and the +// attribute is absent (ctxRootOpen omits it on an empty value). Every route= on every surface comes from here. +template +inline std::string routeNoteOf( const RouteChoiceT& rc, const queryshape::Verdict& shape, bool noRoute ) +{ + return noRoute ? std::string() : rc.reason + shapeDemotionNote( shape ); +} + inline std::string shapeDemotionNote( const queryshape::Verdict& shape ) { if( !shape.fires() ) diff --git a/src/graphlegend.h b/src/graphlegend.h index 95d4c53f7..19fabf248 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -304,6 +304,29 @@ inline constexpr std::string_view kForIdRouteLegend = inline constexpr std::string_view kForRouteCodeLegend = "; route= name-exact(X)|subtoken+body[:broad|:declined]"; +// ONE decision about what these two readings ARE for a given answer. Two surfaces APPEND them (the CLI lens's +// forLensHeaderText, the MCP `for` twin) and both must then EXEMPT exactly those bytes from the signature-trim +// charge — four sites mirroring one rule by hand, which is the shape that ships a ledger 64 B short and +// underflows a subtraction (verbs_for.h records that exact incident for the confidence clause). The caller asks +// once and uses `.sc`/`.route` to append and `.bytes()` to exempt, so the two can only ever agree. +struct ForIdRouteLegendParts +{ + std::string_view sc; // "" when no served row carries a scope + std::string_view route; // "" when this root carries no route= + + std::size_t bytes() const noexcept { return sc.size() + route.size(); } +}; + +inline ForIdRouteLegendParts forIdRouteLegendParts( bool legendOn, bool scPresent, bool routePresent ) noexcept +{ + if( !legendOn ) + { + return {}; // rung zero took the readings; the attributes stay and the dropped note names them + } + return { scPresent ? kForIdRouteLegend : std::string_view(), + routePresent ? kForRouteCodeLegend : std::string_view() }; +} + inline const char* forRootRelPathsLegendShort( bool rootOn, bool atOn = false ) noexcept { if( !rootOn ) diff --git a/src/main.cpp b/src/main.cpp index 66c556346..e19d59311 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1845,6 +1845,17 @@ 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 += ""; + } + // 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/mcpverbs.h b/src/mcpverbs.h index 216840345..929f6d0ab 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1706,7 +1706,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 } ); } @@ -1790,7 +1793,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() : ( rc.reason + shapeDemotionNote( shape ) ), // row 6: the route CODE, as the CLI twin + 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() == '>' ) { @@ -3581,7 +3584,7 @@ inline std::string packTaskText( const std::string& root, const std::string& tas lr.rank = ( rc.which == LexMode::NameExact ) ? lexicalScoresNameExactRanked( ing, task, &tierMul ) : lexicalScoresTiered( ing, g.outOff, g.outTargets, task, 0, &ifaceExact, &tierMul ); // §L10b + verify-wave2 F6: same trim as the other route= construction sites — neither bracket. - lr.routeNote = noRoute ? std::string() : ( rc.reason + shapeDemotionNote( shape ) ); // row 6: the route CODE, as the CLI twin + lr.routeNote = routeNoteOf( rc, shape, noRoute ); // row 6: the route CODE, ONE producer (filter.h) if( !noRoute && !std::getenv( "RIPWIRE_NO_MENTION" ) ) { diff --git a/src/packtask.h b/src/packtask.h index e3ae0d955..1f4b338a0 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1563,7 +1563,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // after the headroom factor, and is always the smaller of the two. Same expression as the XML line // below, so the two serializations cannot report different ceilings. { char b[ 128 ]; rw::formatTo( b, sizeof( b ), ",\"budget_tokens\":{},\"budget_bytes\":{},\"budget_ceiling_bytes\":{}", - budgetTokens, bundleBudget, std::size_t( double( budgetTokens ) * rw::kMinBytesPerToken ) ); j += b; } + budgetTokens, bundleBudget, rw::declaredByteCeiling( budgetTokens ) ); j += b; } // R2: the SAME distance mask the XML used (eligibleIds only) — one eligibility decision, two shapes. j += std::string( ",\"ranking_capped\":" ) + ( sigsCapped ? "true" : "false" ) + ",\"ranking\":"; @@ -1676,7 +1676,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, }; std::string report = "budget="; { char b[ 160 ]; rw::formatTo( b, sizeof( b ), "{} bytes ({}-token target, ceiling {}) | ", - bundleBudget, budgetTokens, std::size_t( double( budgetTokens ) * rw::kMinBytesPerToken ) ); report += b; } + bundleBudget, budgetTokens, rw::declaredByteCeiling( budgetTokens ) ); report += b; } report += std::string( "ranking: " ) + ( sigsCapped ? "capped" : "full" ) + " | "; report += "bodies: " + listStatus( bodiesTotal, bodiesStr, bodiesKept ) + ( bodiesTotal > 0 && !bodiesStr.empty() && bodiesKept < bodiesTotal ? " (capped)" : "" ) + " | "; report += "callers: " + listStatus( callersTotal, callersStr, callersKept ) + " | "; @@ -1784,6 +1784,15 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, const std::size_t rootAttrsBound = rootAttrsFor( whole, /*lastRungFired=*/true ).size(); const rw::CeilingLadderChoice chosen = climbCeilingLadder( buildHeader, headerStr, whole.size() - headerStr.size() + in.trailingSectionBytes + rootAttrsBound, + // TWO CEILINGS (PR #215 review item 1). This root labels itself + // over_ceiling="1" on `estTokens > budgetTokens` — priced at + // kBytesPerTokenDefault — while every rung was judged at + // kMinBytesPerToken x 1.15, so this lens had --for's defect in the + // same words: a bundle whose root will say it overflowed kept its + // verbatim task echo because the echo rung was against a ceiling + // 15% looser than the verdict. The free rungs now aim at what the + // root promises; route= and the label keep the tolerance. + rw::ceilingBytes( budgetTokens ), rw::ceilingAllowanceBytes( budgetTokens ), /*hasRouteAttr=*/!lr.routeNote.empty(), kNotes ); if( chosen.header != headerStr ) diff --git a/src/serialize.h b/src/serialize.h index 11ee6bad0..3a8b7ae03 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -645,6 +645,35 @@ inline constexpr std::size_t ceilingAllowanceBytes( std::size_t budgetTokens ) n return std::size_t( double( budgetTokens ) * kMinBytesPerToken * kCeilingFirstEntryTolerance ); } +// THE EXACT CEILING, IN THE UNIT THE ROOT PRINTS. Both task lenses label a root over_ceiling="1" on +// `est_tokens > budget_tokens`, and est_tokens prices the delivered document at kBytesPerTokenDefault — so the +// largest document that keeps the root silent is budgetTokens x kBytesPerTokenDefault, NOT the allowance above +// and NOT budgetTokens x kMinBytesPerToken. +// +// WHY THIS EXISTS (2026-09-13, PR #215 review item 1). The ladder's free rungs were priced at kMinBytesPerToken +// (2.36) while the verdict they exist to avoid is priced at kBytesPerTokenDefault (2.50), a 6% disagreement in +// the direction that makes the lens trim a document its own root calls conformant. MEASURED on the pre-fix +// binary: `test/cppqualfix --for="widget ping make box" --token-budget=1200` printed est_tokens="778" with no +// over_ceiling= — comfortably inside its budget — and had still dropped three legend clauses "(ceiling)"; at +// --token-budget=1300 the same query kept every clause at est_tokens="1146". A lens must not pay a rung for a +// ceiling it is not against. The tolerance above is for the residual a lens cannot trim (a first signature is +// not divisible) and still governs the rungs that COST something — see climbCeilingLadderBy, which takes both. +inline constexpr std::size_t ceilingBytes( std::size_t budgetTokens ) noexcept +{ + return std::size_t( double( budgetTokens ) * kBytesPerTokenDefault ); +} + +// The conservative hard byte ceiling a token target implies at the DENSEST language rate — a different number +// from ceilingBytes above, with a different job: --pack-task PUBLISHES it (`budget_ceiling_bytes` in the JSON +// dialect, "ceiling C" in the ledger line) so a consumer can check a bundle without re-deriving a rate. It was +// open-coded at three sites in packtask.h; one expression now, so the published number and the ledger's number +// cannot drift. Deliberately NOT repointed at ceilingBytes: this one is what the lens declares to a caller, and +// changing its rate would change a published contract that no defect asks to move. +inline constexpr std::size_t declaredByteCeiling( std::size_t budgetTokens ) noexcept +{ + return std::size_t( double( budgetTokens ) * kMinBytesPerToken ); +} + // The SHAPING budget the same token count buys — tokens x the densest-language byte rate x the headroom. // Distinct from the allowance above (which spends kCeilingFirstEntryTolerance, an OVERSHOOT bar) and // deliberately adjacent to it, so the two are read together and never confused. @@ -725,11 +754,24 @@ struct CeilingLadderChoice CeilingRung rung = CeilingRung::AsBuilt; }; -template -inline CeilingLadderChoice climbCeilingLadderBy( BuildFn&& build, std::string_view builtHeader, FitsFn&& fits, bool hasRouteAttr, - const CeilingLadderNotes& notes ) -{ - if( fits( builtHeader ) ) +// TWO CEILINGS, ONE LADDER (2026-09-13, PR #215 review item 1). `fitsExact` is the ceiling the root PROMISES +// (ceilingBytes: est_tokens <= budget_tokens); `fitsAllowance` is that ceiling plus the first-entry tolerance. +// Which rung is judged by which is the whole design: +// (a) as built and (b) echo dropped are judged by fitsExact — the echo is a byte-for-byte duplicate of task=, +// so spending it costs the reader NOTHING, and a document whose own root will say over_ceiling="1" should +// spend every free rung before it says so. Rung zero (the caller's droppable legend clauses, above this +// function in --for) belongs to the same class and uses the same ceiling. +// (c) route= dropped and (d) the honest label are judged by fitsAllowance — (c) is the first UNIQUE-information +// loss and (d) is the verdict, and the tolerance exists precisely so neither fires on a residual a lens +// cannot trim. Trimming real content, or calling a lens failed, at the exact ceiling would spend the +// tolerance the design has always granted. +// Passing the same predicate twice reproduces the pre-#215 single-ceiling ladder exactly, which is what +// climbCeilingLadder's byte-ceiling pair does when a caller gives it one number. +template +inline CeilingLadderChoice climbCeilingLadderBy( BuildFn&& build, std::string_view builtHeader, FitsExactFn&& fitsExact, + FitsAllowanceFn&& fitsAllowance, bool hasRouteAttr, const CeilingLadderNotes& notes ) +{ + if( fitsExact( builtHeader ) ) { return { std::string( builtHeader ), CeilingRung::AsBuilt }; } @@ -738,11 +780,13 @@ inline CeilingLadderChoice climbCeilingLadderBy( BuildFn&& build, std::string_vi // fixed-payload comparison below, but --for's predicate rebuilds and re-prices a whole header through // finishForLensHeader, so the second call was a duplicated fixpoint on every budgeted run. CeilingLadderChoice choice{ build( /*withRouteAttr=*/true, /*withTaskEcho=*/false, notes.echoDropped ), CeilingRung::EchoDropped }; - bool candidateFits = fits( std::string_view( choice.header ) ); + // (b) is enough when it reaches the exact ceiling, and ALSO when it merely lands inside the allowance: the + // residual past the exact ceiling is what the tolerance is for, and nothing a reader would miss buys it back. + bool candidateFits = fitsAllowance( std::string_view( choice.header ) ); if( !candidateFits && hasRouteAttr ) { choice = { build( /*withRouteAttr=*/false, /*withTaskEcho=*/false, notes.echoAndRouteDropped ), CeilingRung::EchoAndRouteDropped }; - candidateFits = fits( std::string_view( choice.header ) ); + candidateFits = fitsAllowance( std::string_view( choice.header ) ); } if( !candidateFits ) { @@ -756,10 +800,14 @@ inline CeilingLadderChoice climbCeilingLadderBy( BuildFn&& build, std::string_vi // the chosen string is the forgery above (`chosen.find( "over_ceiling:" )`, live until 0.6.1). template inline CeilingLadderChoice climbCeilingLadder( BuildFn&& build, std::string_view builtHeader, std::size_t payloadBytes, - std::size_t byteCeiling, bool hasRouteAttr, const CeilingLadderNotes& notes ) + std::size_t exactCeiling, std::size_t allowanceCeiling, bool hasRouteAttr, + const CeilingLadderNotes& notes ) { - return climbCeilingLadderBy( build, builtHeader, - [ & ]( std::string_view header ) { return header.size() + payloadBytes <= byteCeiling; }, + const auto fitsWithin = [ & ]( std::size_t ceiling ) + { + return [ &, ceiling ]( std::string_view header ) { return header.size() + payloadBytes <= ceiling; }; + }; + return climbCeilingLadderBy( build, builtHeader, fitsWithin( exactCeiling ), fitsWithin( allowanceCeiling ), hasRouteAttr, notes ); } @@ -1477,10 +1525,19 @@ inline std::string countFieldIfAbove( std::uint32_t n, std::uint32_t floor, std: // is unchanged, only its spelling shrinks. The legend spells the composition (id = p::sc::n) and every // selector keeps accepting the composed form: test/scroundtripcheck.sh. ONE writer for the map row and // the signature row, so the two can never drift on when sc= appears. +// THE PRESENCE RULE ITSELF, in one place. Four emitters spell sc= — this writer (the map row), sigRowHead's +// string form (the signature row) and the two JSON twins — and each of them re-derived `!s.scope.empty()`. +// A presence rule open-coded at four sites has three chances to be changed in two, and the legend clause that +// DEFINES sc= is now gated on the same question (verbs_for.h, both dialects), which makes it five. One predicate. +inline bool hasScopeAttr( const Symbol& s ) noexcept +{ + return !s.scope.empty(); +} + template inline void writeScopeAttr( W& w, const Symbol& s, std::vector& esc ) { - if( !s.scope.empty() ) + if( hasScopeAttr( s ) ) { w.write( " sc=\"" ); w.write( escapeXml( s.scope, esc ) ); w.write( "\"" ); } @@ -3409,7 +3466,7 @@ inline std::string sigRowHead( const IngestResult& ing, NodeId id, const SigRowF std::string head = lineAttr; head += escapeXml( s.name, esc ); // escapeXml returns a view INTO esc — copy before the next call head += "\""; - if( !s.scope.empty() ) // row 6: the short id — the scope segment only; p= (this row's, or its 's) supplies the rest + if( hasScopeAttr( s ) ) // row 6: the short id — the scope segment only; p= (this row's, or its '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 @@ -4654,12 +4711,25 @@ inline std::vector calleeWalkOrder( NodeId id, const std::vector lines; // every definition line of that name; joined ascending at append time }; inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, @@ -4672,15 +4742,16 @@ inline void collectCalleeNameRow( std::vector& rows, const { if( r.name == cs.name ) { - r.lines += ','; r.lines += lb; merged = true; + r.lines.push_back( cs.line ); merged = true; break; } } if( !merged ) { - rows.push_back( MergedCalleeNameRow { cs.name, lb } ); + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line } } ); } - used += cs.name.size() + 16; + // 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 @@ -4688,14 +4759,24 @@ inline void collectCalleeNameRow( std::vector& rows, const } // …and the rows written out, once the block's walk is complete. -inline void appendMergedCalleeNameRows( std::string& callsBody, const std::vector& rows, std::vector& esc ) +inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& rows, std::vector& esc ) { - for( const MergedCalleeNameRow& r : rows ) + 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 += "\"/>"; } } @@ -5489,9 +5570,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; @@ -5509,13 +5597,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 += ""; @@ -7232,7 +7319,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 ); - if( !s.scope.empty() ) { w.write( ",\"sc\":" ); writeJsonStr( w, s.scope, esc ); } // row 6: the XML sibling's sc=, same presence rule + 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 ); } @@ -7452,7 +7539,7 @@ inline std::string jsonSigRowHead( const IngestResult& ing, NodeId id, std::uint appendJsonStrField( head, ",\"n\":", s.name ); if( !s.scope.empty() ) // row 6: the XML sibling's sc= — keys mirror attribute names one to one { - appendJsonStrField( head, ",\"sc\":", s.scope ); + if( hasScopeAttr( s ) ) { appendJsonStrField( head, ",\"sc\":", s.scope ); } // row 6: one presence rule (hasScopeAttr) } // 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 f0745236c..60a5eeec1 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -16,6 +16,7 @@ #include "model.h" // IngestResult #include "query.h" // isKnownLayerWord — the layer vocabulary --verify enforces at evaluation #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 { @@ -913,7 +914,32 @@ 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. +// +// WHAT DECIDES: rw::legendCompactAppliesTo (cli.h), the SAME list of non-XML surfaces the binary 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 26 are untouched and +// this is a no-op on them. Gate: test/taskroutecheck.sh runs every generated command against the binary. +inline TaskRouteResult classifyRoutes( std::string_view task, const std::string& root, const IngestResult& ing, bool git, bool dirty ); + inline TaskRouteResult classify( std::string_view task, const std::string& root, const IngestResult& ing, bool git, bool dirty ) +{ + TaskRouteResult result = classifyRoutes( task, root, ing, git, dirty ); + 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 ) { TaskRouteResult result; result.facts.git = git; diff --git a/src/tracelocus.h b/src/tracelocus.h index c8340c66e..3b54f2f2a 100644 --- a/src/tracelocus.h +++ b/src/tracelocus.h @@ -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 b8d858b47..1efdbb993 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 = rc.reason + shapeDemotionNote( shape ); // row 6: the code itself, no "routed: " prose prefix + 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) @@ -491,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 @@ -590,31 +613,62 @@ using rw::deriveForConfidence; // 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=, sc=/route= 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 kForCompactLegendConfidence 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 kForCompactLegendConfidence 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=, route= 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]"; +// 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 ) +{ + std::string note = " [legend clauses: confidence=/margin_pct="; + if( thin ) + { + note += "/coverage="; + } + if( !compact ) + { + note += ", budget_tokens=/max_tokens="; + } + std::string identity; // the sc= / route= readings, named only where they actually rode + if( !compact && scWasOn ) + { + identity = "sc="; + } + if( routeWasOn ) + { + identity += identity.empty() ? "route=" : "/route="; + } + if( !identity.empty() ) + { + note += ", " + identity; + } + note += " and r=/tail (total= shown= capped=) dropped (ceiling) - the attributes stay; a wider token-budget defines them]"; + return note; +} // A1′ (owner decision 2026-09-12): --for's COMPACT legend, pinned at 500 B (test/compactlegendcheck.sh, the // ripwire.for/v1 row) and PRESENT-ONLY — every clause below is emitted only when the document carries the @@ -637,6 +691,12 @@ inline constexpr std::string_view kForCompactLegendRoot = inline constexpr std::string_view kForCompactLegendRows = "d: cx= ccx= complexity, in= callers, churn= amp= change, clone= tested= 1, sc= scope, id=p::sc::n; " "total= shown= capped=1 if cut"; +// …and the same clause for a bundle whose served rows carry NO scope (a corpus of free functions, a markdown +// tree): 22 B of sc= reading removed, nothing else. Two constants rather than one built at runtime because this +// dialect's whole contract is a pinned byte count per schema, and a pin reads a constant. +inline constexpr std::string_view kForCompactLegendRowsNoScope = + "d: cx= ccx= complexity, in= callers, churn= amp= change, clone= tested= 1; " + "total= shown= capped=1 if cut"; // ONE spelling for both dialects (graphlegend.h kForRouteCodeLegend): the compact dialect and the default one // say the same thing about route=, so they cannot drift into two readings of one code. inline constexpr std::string_view kForCompactLegendRoute = rw::kForRouteCodeLegend; @@ -695,11 +755,21 @@ inline bool forRouteAttrPresent( const ForLensHeaderParts& p, bool withRouteAttr return withRouteAttr && p.rootOpenStr.find( " route=\"" ) != std::string_view::npos; } +// …and the THIN question, asked the same way: does this root carry L-W's coverage=? Four sites spelled the +// same find() (two legend appends and two dropped-note selections), which is three chances to change it in two. +inline bool forCoverageAttrPresent( const ForLensHeaderParts& p ) noexcept +{ + return p.confidenceAttrs.find( " coverage=\"" ) != std::string_view::npos; +} + inline void appendCompactForLegend( std::string& h, const ForLensHeaderParts& p, bool withRouteAttr, std::string_view extraNotes ) { h += ""; @@ -2201,11 +2279,23 @@ std::optional runForLens( const MainDispatch& d ) // degrade (surfaceOff — no attribute either) and an exhausted explicit ceiling (legendOff — the // root ATTRIBUTE is kept, paid for out of its own reserve). A tight explicit budget no longer // turns the disclosure off on EITHER serving shape — test/fordisclosurecheck.sh. + // PR #215 review: does any row this bundle could serve carry sc=? Both dialects' sc= readings are + // present-only on this, and the dropped-legend note names sc= only when there was a reading to lose. + // Decided from the RANKED SET and before the header is built, because the un-budgeted path ships the + // header built here and never rebuilds it. Deliberately an OVER-approximation: the byte ladder may still + // trim 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 forScPresent = false; + for( std::size_t i = 0; i < ing.symbols.size() && !forScPresent; ++i ) + { + forScPresent = lensRank[i] > 0 && rw::hasScopeAttr( ing.symbols[i] ); + } ForLensHeaderParts headerParts{ cfg.forTask, rootOpenStr, taskNote, adaptiveNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, floorNote, forConf.attrs, forConf.note, forAtAttrStr, mentionDocAttrsStr, cfg.anchor, plan.autoBodies, plan.compact, cfg.legend == "compact", - /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg }; + /*tailLegend=*/true, /*idRouteLegend=*/true, /*legendDropped=*/false, flRootArg, + forScPresent }; const auto buildForHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { return forLensHeaderText( headerParts, withRouteAttr, withTaskEcho, extraNotes ); }; std::string headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); @@ -2453,9 +2543,12 @@ std::optional runForLens( const MainDispatch& d ) // A1′: the compact dialect emits ONLY the route clause here (sc= rides its always-on row clause), and only when // the root carries route= — subtract exactly what appendCompactForLegend appended. The DEFAULT dialect emits the // sc= rule always and the same route clause under the same present-only condition (forRouteAttrPresent). - const std::size_t routeCodeEmitted = routeNoteRaw.empty() ? 0u : rw::kForRouteCodeLegend.size(); - const std::size_t idRouteLegendEmitted = !headerParts.idRouteLegend ? 0u - : ( compactLegendOn ? routeCodeEmitted : rw::kForIdRouteLegend.size() + routeCodeEmitted ); + // …and the SAME decision the append made (rw::forIdRouteLegendParts), so the ledger cannot subtract a + // clause the header never wrote. The compact dialect emits only the route reading here — its sc= reading + // rides the always-on rows clause — so it exempts only that part. + const rw::ForIdRouteLegendParts idRouteParts = + rw::forIdRouteLegendParts( headerParts.idRouteLegend, headerParts.scPresent, !routeNoteRaw.empty() ); + const std::size_t idRouteLegendEmitted = compactLegendOn ? idRouteParts.route.size() : idRouteParts.bytes(); const std::size_t exemptBytes = adaptiveNote.size() + autoLegendBytes + confidenceExemptBytes + tailLegendEmitted + idRouteLegendEmitted; if( exemptBytes > headerStr.size() ) { @@ -2798,11 +2891,18 @@ std::optional runForLens( const MainDispatch& d ) // ladder has not answered yet. M3's first version passed the candidate's rung in and set the flag from // `rung == OverCeiling`, which no call could ever satisfy — the terminal rung is the branch that never // asks `fits` — so the flag was invariantly false while a comment claimed it was doing the pricing. - const auto fitsCeiling = [ & ]( std::string_view candidate ) + // ONE fit test, parameterised by the ceiling it is against (PR #215 review: this was two lambdas + // spelling the same two comparisons, which is how they came to disagree on the RATE). + const auto fitsWithin = [ & ]( std::size_t ceiling ) { - return candidate.size() + ladderPayloadBytes <= ladderCeiling - && finishForLensHeader( std::string( candidate ), rootFinish ).size() + emittedNonHeaderBytes <= ladderCeiling; + return [ &, ceiling ]( std::string_view candidate ) + { + return candidate.size() + ladderPayloadBytes <= ceiling + && finishForLensHeader( std::string( candidate ), rootFinish ).size() + emittedNonHeaderBytes <= ceiling; + }; }; + const auto fitsCeiling = fitsWithin( ladderCeiling ); + const auto fitsExactCeiling = fitsWithin( rw::ceilingBytes( cfg.tokenBudget ) ); // RUNG ZERO — the confidence LEGEND clause, before any of the ladder's own rungs: it is the one // header string whose loss costs NO unique information (confidence=/margin_pct= stay on the root // as facts; only their explanation goes), so it must fall before the verbatim task echo does — @@ -2816,32 +2916,46 @@ std::optional runForLens( const MainDispatch& d ) // kForLegendDroppedNote, naming the attributes whose definitions just went. Silence here was the // reader seeing confidence= margin_pct= budget_tokens= r= and the counts with nothing in the // legend about any of them and no way to tell a budget cut from a feature that does not exist. - // RUNG ZERO TRIGGERS ON THE EXACT CEILING, not on the 1.15 allowance the ladder's own rungs and the - // over_ceiling verdict are judged by (2026-09-13, PR #215 CI). kCeilingFirstEntryTolerance exists for the - // residual a lens cannot trim — a first signature that is not divisible — and it was also gating this - // rung, so a document 1..15% over its budget that STILL carried three droppable clauses shipped over_ceiling=1 - // with all three riding: fornotesbudgetcheck's 1640 rung at est_tokens=1755 and forrootlegendcheck's 800 - // rung at 831 on CI, where the slack a different fixture path leaves is a few dozen bytes. The free drop - // is tried first, at the number the root promises; only what remains after it is measured against the - // tolerance. Same fit test otherwise (the reserve-priced header plus every other byte stdout receives). - const auto fitsExactCeiling = [ & ]( std::string_view candidate ) - { - const std::size_t exactCeiling = std::size_t( double( cfg.tokenBudget ) * rw::kMinBytesPerToken ); - return candidate.size() + ladderPayloadBytes <= exactCeiling - && finishForLensHeader( std::string( candidate ), rootFinish ).size() + emittedNonHeaderBytes <= exactCeiling; - }; - if( !fitsExactCeiling( headerStr ) && ( !headerParts.confidenceNote.empty() || headerParts.tailLegend || headerParts.idRouteLegend ) ) + // RUNG ZERO TRIGGERS ON THE EXACT CEILING, not on the 1.15 allowance rungs (c)/(d) are judged by + // (2026-09-13, PR #215). kCeilingFirstEntryTolerance exists for the residual a lens cannot trim — a first + // signature that is not divisible — and it was also gating this rung, so a document 1..15% over its budget + // that STILL carried three droppable clauses shipped over_ceiling=1 with all three riding + // (fornotesbudgetcheck's 1640 rung at est_tokens=1755, forrootlegendcheck's 800 rung at 831, both CI). + // …AND "EXACT" IS THE ROOT'S OWN RATE, not the densest-language one (review item 1). This test priced at + // kMinBytesPerToken(2.36) while est_tokens=/over_ceiling= price at kBytesPerTokenDefault(2.50), so the rung + // fired on documents 6% INSIDE the budget their root reports: `test/cppqualfix --for="widget ping make box" + // --token-budget=1200` printed est_tokens="778", no over_ceiling=, and had dropped all three clauses. + // rw::ceilingBytes is the one expression that answers "what does this root promise", and the ladder below + // takes it for rungs (a)/(b) for the same reason. + // + // ONE DROPPABLE BIT. These three fields moved in lock step at every read and write, and the guard that used + // to test all three was true whenever any clause remained, which is the only state this branch is reached + // in — an always-true condition documenting itself as a check. legendDroppable() asks the one question. + // + // AND THE DROP MUST PAY (review item 3). Rung zero removes readings and splices the note that names + // them, and in the COMPACT dialect the note is longer than the clauses: measured on the pre-fix binary, + // 110-164 B of clauses went out and a 161 B note came in — +51 B net on a route-less answer, so the rung + // made the very document it was trying to shrink BIGGER while costing the reader three definitions. The + // candidate is built and compared; a drop that does not pay is not taken, and the readings simply ride. + // This is the assertion, not a comment about one: there is no shape in which the byte-negative branch + // can be reached, because the branch is the comparison. + if( headerParts.legendDroppable() && !fitsExactCeiling( headerStr ) ) { - headerParts.confidenceNote = {}; - headerParts.tailLegend = false; // deep-tail: the explainer falls with the confidence clause — - // the r= attrs and the element (the facts) survive - headerParts.idRouteLegend = false; // row 6: the sc=/route= reading falls with them — the attributes stay - headerParts.legendDropped = true; // …and this is the sentence that says both of them fell - headerStr = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + const ForLensHeaderParts keptParts = headerParts; + headerParts.dropDroppableLegend(); + std::string droppedHeader = buildForHeader( /*withRouteAttr=*/true, /*withTaskEcho=*/true, {} ); + if( droppedHeader.size() < headerStr.size() ) + { + headerStr = std::move( droppedHeader ); + } + else + { + headerParts = keptParts; // byte-negative: keep the readings and say nothing + } } // M3: the ladder hands back the rung it took. That value — never a search of the emitted text — is // what puts over_ceiling="1" on the root below. - rw::CeilingLadderChoice chosen = rw::climbCeilingLadderBy( buildForHeader, headerStr, fitsCeiling, + rw::CeilingLadderChoice chosen = rw::climbCeilingLadderBy( buildForHeader, headerStr, fitsExactCeiling, fitsCeiling, /*hasRouteAttr=*/!routeNoteRaw.empty(), kNotes ); headerStr = std::move( chosen.header ); rootFinish.lastRungFired = ( chosen.rung == rw::CeilingRung::OverCeiling ); diff --git a/test/attrvocabcheck.sh b/test/attrvocabcheck.sh index c69b90e38..a5c59f96e 100755 --- a/test/attrvocabcheck.sh +++ b/test/attrvocabcheck.sh @@ -250,8 +250,13 @@ if not rows: print( " .. 8) no safe-delete caller row carries amb= on this corpus — the value arm cannot run" ); sys.exit( 0 ) checked = 0 for name, path, line, k in rows[ :6 ]: - # map rows: , the id's file half being the same root-relative path - cands = re.findall( r']*n="' + re.escape( name ) + r'" id="' + re.escape( path ) + r'::[^"]*"[^>]*>', mp ) + # ROW 6 (PR #215 review item 7): a map row is inside an wrapper — + # the path-repeating id= this arm matched on is GONE, so the regex found nothing, `checked` stayed 0, and the + # arm printed a PASS line about 0 cross-checks. A gate that reports success for work it skipped is the + # harness trap this suite has a name for; the arm now fails on checked == 0 instead of congratulating itself. + # Matched by NAME within the block that owns it, which is how the composed id p::sc::n is spelled now. + block = re.search( r']*>(.*?)(?=]*>', block.group( 1 ) ) if block else [] if not cands: continue checked += 1 ks = { ( re.search( r' amb="(\d+)"', c ).group( 1 ) if re.search( r' amb="(\d+)"', c ) else "0" ) for c in cands } @@ -259,8 +264,11 @@ for name, path, line, k in rows[ :6 ]: print( f" .. 8) {name} @ {path}:{line}: safe-delete amb=\"{k}\" but the map row(s) say amb={sorted(ks)}" ); sys.exit( 1 ) if k == "1" and all( x == "1" for x in ks ) and len( rows ) > 1: pass +if not checked: + print( " .. 8) 0 safe-delete caller rows could be matched to a map row — this arm proved NOTHING and is not a pass" ) + sys.exit( 1 ) print( f" .. 8) {checked} safe-delete caller row(s) cross-checked against their map rows" ) -sys.exit( 0 if checked else 0 ) +sys.exit( 0 ) PY8 # the boolean spelling must be gone: a caller with SEVERAL ambiguous calls says so if grep -qE ']* amb="([2-9]|[1-9][0-9]+)"' "$TMP/sd.xml"; then diff --git a/test/routehookcheck.sh b/test/routehookcheck.sh index 01add7459..cf22ead3b 100755 --- a/test/routehookcheck.sh +++ b/test/routehookcheck.sh @@ -496,5 +496,62 @@ 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 .' +# 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' +[ "$o9_bad" -eq 0 ] \ + && ok "O9 command-word rule: $o9_n shapes read correctly (13 wrapped/sequenced calls, 5 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/scroundtripcheck.sh b/test/scroundtripcheck.sh index 17bdf5ced..a1f95b3fd 100755 --- a/test/scroundtripcheck.sh +++ b/test/scroundtripcheck.sh @@ -143,4 +143,35 @@ if grep -q ']* id="' "$TMP/for.xml"; then no "(F) a row still prints i "$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 )" + [ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "ALL FAIL"; exit 1; } diff --git a/test/skilltruthcheck.sh b/test/skilltruthcheck.sh index e1867aaa4..48f29b093 100755 --- a/test/skilltruthcheck.sh +++ b/test/skilltruthcheck.sh @@ -174,30 +174,53 @@ 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 for an XML verb carries -# --legend=compact; --for is exempt (its compact legend is its own and the first call of a session wants the full -# one), and so are the text/JSON/writer verbs the binary refuses the flag on. The list is the shipped policy — -# the same list the 2026-09-12 transform applied — spelled here so a skill edit that drops the flag is red. -SKILL_COMPACT_VERBS="callers callees impact uses expand around path connect mentions at exemplar lego pack-task pack-signatures from-trace edit-check safe-delete verify whereis affected test-gate quality-delta quality-panel hotspots clones lint seams deps communities community zoom tree cochange owners readability ensemble context-ratio naming-consistency comment-coherence nonlocal-state dead-code doc-drift notes stray-content skipped flags metrics grep regex match query map-diff pr-context external-surface exercises doctor dmm help-task merge-scout plan-lint scan-skills handoff layout graph-query slice outline" +# 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='`' -missing=0; checked=0 -for _v in $SKILL_COMPACT_VERBS; do - while IFS= read -r line; do - [ -n "$line" ] || continue - checked=$(( checked + 1 )) - # the command span: from `ripwire --VERB` to the closing backtick (or end of line) - span="$( printf '%s\n' "$line" | grep -oE -- "ripwire --$_v(=|$BT|[[:space:]]|\$)[^$BT]*" | head -1 )" - case "$span" in *'--legend=compact'*) ;; *) missing=$(( missing + 1 )); [ $missing -le 5 ] && printf ' %s: %s\n' "$_v" "$( printf '%s' "$span" | head -c 120 )";; esac - done <<<"$( grep -rhE -- "ripwire --$_v(=|$BT|[[:space:]]|\$)" "$ROOT/skills" --include='*.md' )" -done -[ "$checked" -gt 0 ] || no "skills compact policy: no command spelled for any policy verb — the arm inspected nothing" -[ "$missing" -eq 0 ] \ - && ok "skills compact policy: all $checked \`ripwire --VERB\` commands on XML verbs carry --legend=compact" \ - || no "skills compact policy: $missing of $checked XML-verb commands in skills/ lack --legend=compact" -if grep -rhE -- 'ripwire --for=[^`]*--legend=compact' "$ROOT/skills" --include='*.md' | grep -q .; then - no "skills compact policy: a --for command carries --legend=compact (exempt: the first call wants the full legend)" -else - ok "skills compact policy: no --for command carries --legend=compact" -fi +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" [ "$fail" -eq 0 ] && echo "ALL PASS" || { echo "SOME CHECKS FAILED"; exit 1; } diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 80cb250ee..f3a35afef 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -381,5 +381,40 @@ esac EVAL="$( python3 "$ROOT/bench/taskroute_eval.py" --bin "$BIN" --corpus "$ROOT/test/taskroutefix/prompts.tsv" --split test 2>&1 )"; rc=$? if [ "$rc" -eq 0 ]; then ok "held-out command-routing floors ($EVAL)"; else no "held-out command-routing floors failed: $EVAL"; fi + +# ── R-LEG: every command this router GENERATES is one the binary accepts (PR #215 review item 5) ─────────── +# A1-2 put --legend=compact on the route commands by editing 26 strings. That is 26 chances to be wrong and no +# rule for the 27th, and the same hand-application shipped `--zoom --legend=compact --mermaid` into a skill — +# a command the binary REFUSES. classify() applies the posture once now (rw::legendCompactAppliesTo), and this +# arm is what makes that a fact rather than an intention: every 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" +rleg_n=0; rleg_bad=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" || true + if grep -q 'applies to the XML verbs only' "$TMP/rleg.err"; then + rleg_bad=$(( rleg_bad + 1 )) + [ "$rleg_bad" -le 5 ] && printf ' REFUSED: %s\n' "$( 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)" + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" From 2bdad93a243586fc8f34bb01f5ca6bb6241277a0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 13:39:04 -0400 Subject: [PATCH 13/22] docs(sc): rebuild COMMANDS.md from a ref-clean capture, and make every number in the text the number the gate reads MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review items 9 (the removed shape surviving in shipped text) and 10 (text against shipped numbers), plus the present-only consequences the earlier commit set up. THE CAPTURE, RECORDED REF-CLEAN. docs/COMMANDS.md is built from a showcase capture, and its samples still showed the id= rows and the `route="routed: …"` prose row 6 retired — 96 id= rows and 19 `id=canonical(…)` legends in a document that is the reference for what this tool prints. Regenerating in place was not available: this machine holds 330 local branches and test/showcase_capture.py runs `--stray-content=lane` against the real repository, so a regenerated capture publishes whatever branch names the machine has (the trap this repo already records; main's own capture carries 8). So the capture was recorded in a CLONE made with `git clone --single-branch --branch lane/sc-legend`, with the three local scratch tags deleted — 20 refs down to one branch and the release tags. The result carries exactly one branch name, `lane/sc-legend`, which is pushed and public; the two other `lane/*` strings in it are committed .ripwire_notes content that main's capture carries too, and `codex-lane/v1` is fixture JSON. Scrub checks on the written file: 0 home-directory paths, 0 scratch paths, 0 private-corpus names. docs/COMMANDS.md rebuilt from it (253 commands recorded, 161 samples used): id= rows 96 -> 26, and all 26 are JSON-RPC ids and cluster ids (T1/T2), none a symbol; the `id=canonical(…)` legends 19 -> 1, which is --uses's `in_id=` — that one names a DIFFERENT symbol than the row, so it does not repeat the row's own p= and is correct; `routed:` 11 -> 1, the compact dialect's own routed note, which is current. The dated captures before this one are kept: they are what the binary printed on their date, and deleting them would be rewriting the record rather than correcting it. --expand's WHOLE-FILE serving gained its compact schema, ripwire.expand-file/v1. The one `ripwire.expand/v1` purpose line described `//` — elements the whole-file document does not contain — and compactlegendcheck's own "compact must shrink" arm caught it as the contradiction it is: 265 B of compact legend against 236 B of full. Two servings, two purpose lines, chosen off the root's own mode=. EVERY NUMBER AGAINST ITS GATE: * CHANGELOG said `--for`'s compact legend is "pinned at 500 bytes" (heading and body) where the gate reads 670; said "help_all alone, 41 labels unchanged", which was true of the merge step and not of the lane (seven of 42 moved); and gave three per-call compact deltas with no task and no method. All three fixed, and the deltas re-measured and named: `pagerank power iteration` 9,881 -> 9,344 B (25 -> 29 signature rows), `rank graph teleport` 10,127 -> 9,818 (22 -> 25), `escapeXml` 5,823 -> 4,601. * compactlegendcheck's header said "<= 5,000 B" where its arm reads 4,700, and three sites still said 500. * THE PIN TABLE DID NOT FOLLOW ITS OWN STATED RULE. "Largest measured (U) probe, up to the next multiple of 10 B, plus 10" — seven rows disagreed with it, most visibly pack-task at 980 over a measured 974 where the rule says 990. Every schema was re-probed on this build and every pin recomputed from its own measurement, so the pin column is now derivable from the measured column: map 892 -> 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, for 654 -> 670, expand-file 230 -> 240 (new). * legendcoveragecheck's (F) arm had printed "4 pinned shared-name closures no longer reproduce — verify, then shrink the FLOOR" for four runs without anyone shrinking it. Shrunk: this dialect's rung zero no longer fires on those documents, because it is taken only when the drop pays. GOLDENS, AND THE ROUND TRIP THAT PROVES THE RULE. test/anchorfix and test/routefix serve free functions, which have no enclosing scope, so under the present-only rule the sc= reading has nothing to define and does not ride: both goldens are byte-identical to their PRE-LANE selves (3,557 B and 3,776 B), and the +29 B re-pin this lane recorded three commits ago is retired rather than carried. Both probes are --no-route, so the route= reading is absent too — one fixture controlling both halves of the same rule. printf_parity re-pinned for `expand` alone (UPDATE_GOLDEN_EXPECT matched), 41 labels unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- CHANGELOG.md | 95 +- docs/COMMANDS.md | 957 ++- docs/captures/COMMANDS_showcase_2026-09-13.md | 7152 +++++++++++++++++ src/compactlegend.h | 13 +- src/verbs_for.h | 2 +- test/anchorcheck.sh | 12 +- test/anchorfix/golden_for.xml | 2 +- test/compactlegendcheck.sh | 45 +- test/legendcoveragecheck.sh | 7 +- test/printf_parity.manifest | 2 +- test/routecheck.sh | 9 +- test/routefix/golden_for.xml | 2 +- 12 files changed, 7778 insertions(+), 520 deletions(-) create mode 100644 docs/captures/COMMANDS_showcase_2026-09-13.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 505ff6819..0f06bf3d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,7 +50,7 @@ past leading `VAR=value` assignments) — never a directory argument or a bare w `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 pinned at 500 bytes +### 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 @@ -64,11 +64,16 @@ 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 → 494 B, pinned at 500 as the new -`ripwire.for/v1` row, the exemption gone. Per call on this tree, three tasks under `--legend=compact`: -9,947 → 9,669, 10,075 → 9,682 and 5,156 → 4,624 B (−278, −393, −532 B) with the same signature rows -served (32, 26, 3). The MCP `for` twin declares no `legend` field and serves the default dialect only, -so its bytes are unchanged (9,132, 9,149 and 2,173 B on the same tasks). `legendcoveragecheck` holds: +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 on this repository, default legend → `--legend=compact`, three +tasks measured with `wc -c` on this build: `pagerank power iteration` 9,881 → 9,344 B (−537, and 25 → 29 +signature rows, because the bytes the legend gives back are spent on rows), `rank graph teleport` +10,127 → 9,818 B (−309, 22 → 25 rows), and the name-exact `escapeXml` 5,823 → 4,601 B (−1,222). 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. @@ -122,7 +127,9 @@ because A1′'s present-only `--for` legend outweighs what both lanes added); th `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, 41 labels unchanged. +re-pinned for `help_all` alone at the merge (`UPDATE_GOLDEN_EXPECT` matched, 41 labels unchanged); across +the whole lane seven of its 42 labels moved — six for the row-6 row shape and `help_all` once more for the +merged `--help` text. ### Fixed — `--for`'s rung zero fires on the exact ceiling, not on the overshoot allowance @@ -179,6 +186,80 @@ 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 + +Ten 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="1149"` +at `--token-budget=1200` with every clause riding. No tolerance was widened and no ceiling was raised. + +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. + ### 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/docs/COMMANDS.md b/docs/COMMANDS.md index 9cb5f5416..b796f2fdc 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` @@ -284,18 +287,19 @@ _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):** @@ -316,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` @@ -348,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):** @@ -378,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] ``` @@ -410,7 +417,7 @@ $ ./build/ripwire . --lego=Vehicle - + @@ -439,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 ) @@ -469,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 @@ -534,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` @@ -564,7 +576,7 @@ _File-by-file orientation map (top symbols per file)._ $ ./build/ripwire . --tree - + @@ -634,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] @@ -665,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] ``` @@ -698,7 +710,7 @@ _Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says ``` $ ./build/ripwire . --around=rankGraphTeleport - + @@ -706,10 +718,10 @@ $ ./build/ripwire . --around=rankGraphTeleport - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -752,7 +764,7 @@ _What SYM calls (1-hop out-edges)._ $ ./build/ripwire . --callees=rankGraphTeleport - + @@ -785,7 +797,7 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count $ ./build/ripwire . --uses=rankGraphTeleport - + @@ -819,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))' - + - - + + - + @@ -856,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] ``` @@ -892,10 +904,10 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d $ ./build/ripwire . --path=main,rankGraphTeleport - - - - + + + + @@ -916,7 +928,7 @@ $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + @@ -942,16 +954,16 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - - - + + - + @@ -1027,7 +1039,7 @@ _Test files that transitively reach the changed file._ $ ./build/ripwire . --affected=src/graph.h - + @@ -1087,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) ``` @@ -1106,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] ``` @@ -1141,7 +1153,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1164,7 +1176,7 @@ _Regex search + enclosing symbol._ ``` $ ./build/ripwire . --regex='fnv1a\w+' - @@ -1193,7 +1205,7 @@ _Same search with one line of source context either side._ ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --grep-context=1 - @@ -1222,7 +1234,7 @@ _Boolean grep: hits where BOTH literals share the matched line (--grep-scope=lin ``` $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --and=cache - @@ -1251,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 - @@ -1282,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 - @@ -1315,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 - @@ -1352,7 +1364,7 @@ _h= on each editable enclosing-symbol row: a freshness-pinned identity an edit v $ ./build/ripwire . --grep=DEGRADED_PATH_ALERT --handles - @@ -1385,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 @@ -1433,17 +1445,17 @@ _Raw BM25 ranking (debug lens; --for is the real verb)._ ``` $ ./build/ripwire . --query="teleport pagerank" --top-k=5 - - + + - - + + - + @@ -1468,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` @@ -1501,19 +1514,19 @@ _Body-elided decl skeletons — recounted on this corpus. Measured as element by ``` $ ./build/ripwire . --pack-signatures --top-k=10 - + - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1562,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 - + + @@ -1704,12 +1717,12 @@ $ ./build/ripwire . --metrics --top-k=10 - - + + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1734,13 +1747,13 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - + + + - - - + + + @@ -1772,16 +1785,16 @@ $ ./build/ripwire . --hotspots - - - - - - - + + + + + + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1799,18 +1812,18 @@ _Token-normalized duplicate bodies._ $ ./build/ripwire . --clones - - + + + + + + + - - - - - ... [17 more line(s); run it to see the whole thing] ``` @@ -1830,15 +1843,15 @@ _Per-function readability, LEAST readable first (Halstead volume, token entropy, $ ./build/ripwire . --readability --limit=8 - - - - - - + + + + + + + - ``` @@ -1864,7 +1877,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1900,17 +1913,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1931,17 +1944,17 @@ $ ./build/ripwire . --quality-panel - - - - - - - - + + - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1967,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] ``` @@ -1998,7 +2011,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2007,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):** @@ -2034,11 +2047,11 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio $ ./build/ripwire . --naming-consistency --limit=8 - - - - - + + + + + @@ -2070,8 +2083,8 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - + + @@ -2079,7 +2092,7 @@ $ ./build/ripwire . --lint --naming-locals - + @@ -2105,9 +2118,9 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2140,16 +2153,16 @@ $ ./build/ripwire . --cochange - - + + - + + + - - ... [22 more line(s); run it to see the whole thing] ``` @@ -2174,16 +2187,16 @@ $ ./build/ripwire . --cochange --cochange-recur=2 - - + + - + - - - + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2202,11 +2215,11 @@ $ ./build/ripwire . --cochange --cochange-groups - + - + @@ -2232,16 +2245,16 @@ $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - - - - - - + + + + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2350,7 +2363,7 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - + @@ -2359,7 +2372,7 @@ $ ./build/ripwire . --lint - + @@ -2460,7 +2473,7 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2469,7 +2482,7 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2547,17 +2560,17 @@ _Cluster the call graph into cohesive modules._ $ ./build/ripwire . --communities - + + - - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2578,7 +2591,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp $ ./build/ripwire . --community=0 - ``` @@ -2603,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] @@ -2640,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] ``` @@ -2667,12 +2680,12 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ $ ./build/ripwire . --seams - - + + + - @@ -2697,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"] @@ -2728,14 +2741,14 @@ $ ./build/ripwire . --owners - - - + + + - + - + @@ -2757,7 +2770,8 @@ _High-confidence internal functions with no caller. NOTE the filter is a path-CO ``` $ ./build/ripwire . --dead-code=src - + + ``` @@ -2821,9 +2835,10 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - + @@ -2833,7 +2848,6 @@ $ ./build/ripwire . --quality-delta - ... [17 more line(s); run it to see the whole thing] ``` @@ -2858,9 +2872,10 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - + @@ -2870,7 +2885,6 @@ $ ./build/ripwire . --quality-delta - ... [17 more line(s); run it to see the whole thing] ``` @@ -2895,7 +2909,7 @@ _The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorde ``` $ ./build/ripwire . --dmm -

@@ -2965,7 +2979,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -2995,7 +3009,7 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - + @@ -3048,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` @@ -3068,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):** @@ -3118,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` @@ -3181,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` @@ -3230,7 +3244,7 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - + @@ -3268,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() );]]> @@ -3348,7 +3362,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - + ``` @@ -3374,11 +3388,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3448,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/ - - - - - - - - - + ``` @@ -3486,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` @@ -3522,7 +3516,7 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - + ``` @@ -3545,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] ``` @@ -3576,7 +3570,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3657,7 +3651,7 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - @@ -3692,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] ``` @@ -3727,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] ``` @@ -3761,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] ``` @@ -3797,7 +3791,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -3835,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 ``` @@ -3862,7 +3856,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -3952,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` @@ -3986,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):** @@ -4019,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` @@ -4094,14 +4093,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + ``` @@ -4127,14 +4126,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4162,7 +4161,7 @@ $ ./build/ripwire . --skipped - @@ -4239,7 +4238,7 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + @@ -4248,10 +4247,10 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 - - + + - + ... [16 more line(s); run it to see the whole thing] ``` @@ -4314,7 +4313,7 @@ _The same gating report under --legend=compact — same rows, same exit 2, schem ``` $ ./build/ripwire . --quality-delta --legend=compact - @@ -4364,17 +4363,17 @@ $ ./build/ripwire . --ensemble --limit=8 - - - - - - - + + + + + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -4396,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] ``` @@ -4427,7 +4426,7 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch ``` $ ./build/ripwire . --map-diff --top-k=5 - + @@ -4435,12 +4434,12 @@ $ ./build/ripwire . --map-diff --top-k=5 - - + + - + - + ... [11 more line(s); run it to see the whole thing] ``` @@ -4461,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] ``` @@ -4511,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] ``` @@ -4542,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] ``` @@ -4576,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 - + - - + + - + @@ -4642,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] ``` @@ -4670,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] @@ -4721,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` @@ -4738,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 / @@ -4768,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] ``` @@ -4826,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/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":"frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit()frobnicate the widget cache after a bulk insert — the LEXICAL ANCHOR for the gate's query ("frobnicate the widget cache"): its name carries every query token, so it tops the BM25 ranking. Its directvoid frobnicateWidgetCache()widget cache lookup — a third lexical hit (two query tokensint widgetCacheLookup( int key )widget cache init — a second, weaker lexical hit (two query tokensvoid widgetCacheInit() rows, --lego's caveat=; on the map family and --impact: pr_iters=, @@ -324,7 +324,7 @@ probeFor() # with attributes still undefined. Each schema's pin is its LARGEST measured (U) probe on this gate's fixture, rounded up to the # next multiple of 10 B, plus 10 B, so a verb whose legend grows is re-pinned in the commit that adds the bytes, with the bytes # attributed there. A schema with no row FAILS: a new XML verb is measured and pinned, never waved through under a default. -# --for's NATIVE compact legend joined the table 2026-09-12 (A1′, owner decision): pinned at 500 B, present-only +# --for's NATIVE compact legend joined the table 2026-09-12 (A1′, owner decision): pinned per the rule below, present-only # definitions — it used to spend 1,177–1,216 B on this fixture and was exempt by name (below). # ONE ROW PER SCHEMA: the pin, then the largest (U) probe it was measured from. Measured 2026-09-12 in the fourth sweep's last # pass, once every attribute the --impact, --safe-delete, --communities, --community and map-header answers print had a reading; @@ -338,19 +338,25 @@ probeFor() # (measured 908, the --rank-by=churn probe), map-diff 800 -> 910 (901), pack-signatures 680 -> 780 (775), metrics 720 -> 820 # (814), query 630 -> 730 (723), pack-task 820 -> 980 (974), pack-top-n 660 -> 770 (761). The bytes the reading costs are the # bytes the rows save: 20 B per scoped row (this repo's flagless map, 137 scoped rows: -4,048 B, -15.3%). -# RE-PINNED 2026-09-13 (merge of lane/sc-legend and lane/for-widen): ripwire.for/v1 500 -> 690, measured 678 on the -# MERGED tree (rule above: largest measured (U) probe, up to the next 10 B, plus 10). A1' pinned this dialect at 500 -# from 494 with only its own clauses present; lane for-widen then added the coverage= reading and the thin rule -# (kForCompactCoverageClause, forpage.h, 168 B) and this gate's --for=geometry probe IS a thin answer, so the probe -# carries it. Measured, not summed. The other seven schemas sc-legend re-pinned were re-measured here too and all -# still fit: map 892 of 920, map-diff 885 of 910, pack-signatures 759 of 780, metrics 798 of 820, query 707 of 730, -# pack-task 974 of 980, pack-top-n 745 of 770. +# RE-MEASURED 2026-09-13 (PR #215 review item 10). The table's own rule above is "the largest measured (U) probe, +# rounded up to the next multiple of 10 B, plus 10", and seven rows did not follow it — pack-task sat at 980 over a +# measured 974 where the rule says 990, and five others sat 10 B tight against numbers that had moved underneath +# them. A rule stated in a comment and not applied by the table is not a rule. Every schema was re-probed on this +# build and every pin recomputed from its own measurement, so the `pin` column is now derivable from the `measured` +# column and a reader can check it: map 892 -> 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, for 654 -> 670. +# +# ripwire.for/v1 in particular: A1' pinned it at 500 from 494 with only its own clauses present; the merge with +# #213 put the coverage= reading on the same probe (this gate's --for=geometry IS a thin answer), and this round +# made the sc= and route= readings present-only. 654 B is the number on the tree that ships, measured three times +# and never held to a pin by trimming a clause. +# ripwire.expand-file/v1 is NEW: --expand has two servings and they share no element (see compactlegend.h). # schema pin measured PIN_TABLE=' -ripwire.map/v1 920 908 -ripwire.map-diff/v1 910 901 -ripwire.pack-signatures/v1 780 775 -ripwire.metrics/v1 820 814 +ripwire.map/v1 910 892 +ripwire.map-diff/v1 900 885 +ripwire.pack-signatures/v1 770 759 +ripwire.metrics/v1 810 798 ripwire.deps/v1 260 245 ripwire.hotspots/v1 280 264 ripwire.clones/v1 290 280 @@ -391,12 +397,12 @@ ripwire.mentions/v1 180 168 ripwire.affected/v1 350 339 ripwire.verify/v1 330 316 ripwire.help-task/v1 170 153 -ripwire.query/v1 730 723 +ripwire.query/v1 720 707 ripwire.grep/v1 360 345 ripwire.match/v1 270 260 ripwire.lego/v1 290 275 ripwire.exemplar/v1 250 232 -ripwire.around/v1 780 760 +ripwire.around/v1 770 760 ripwire.callers/v1 330 317 ripwire.callees/v1 380 369 ripwire.uses/v1 290 271 @@ -409,10 +415,11 @@ ripwire.merge-scout/v1 220 208 ripwire.whereis/v1 240 223 ripwire.community/v1 730 719 ripwire.layout/v1 160 149 -ripwire.pack-task/v1 980 974 -ripwire.pack-top-n/v1 770 761 +ripwire.pack-task/v1 990 974 +ripwire.pack-top-n/v1 760 745 ripwire.expand/v1 280 265 -ripwire.for/v1 690 678 +ripwire.expand-file/v1 240 230 +ripwire.for/v1 670 654 ' pinFor() { @@ -445,7 +452,7 @@ while IFS="$( printf '\t' )" read -r flag kind example policy; do schema="$( leg schema "$TMP/u.c" )" lb="$( leg prose "$TMP/u.c" "$TMP/u.full" )"; lball="$( leg bytes "$TMP/u.c" )"; lbfull="$( leg bytes "$TMP/u.full" )" case "$schema" in ripwire.*/v1) ;; *) no "(U) $probe compact root has no schema=\"ripwire./v1\" (got '$schema')" ;; esac - # A1′ (2026-09-12): --for is pinned like every other verb now (ripwire.for/v1 at 500 B) — its native compact + # A1′ (2026-09-12): --for is pinned like every other verb now (the ripwire.for/v1 row) — its native compact # legend defines only the terms the document carries; the "shrinks vs full" arm below still runs on it. pin="$( pinFor "$schema" )" if [ -z "$pin" ]; then diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index cae931dba..afec0b0b6 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -366,8 +366,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/printf_parity.manifest b/test/printf_parity.manifest index 7a7d8ce85..e0be1812a 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -10,7 +10,7 @@ callers 0 c8317dc738f05616369995c0a0c03a6c12fdea8d2a2c5b7d182156a4de76cdc6 e3b0c impact 0 4009dd7227dabc63a96e6318e77c6f704f25b7c2a31295fa80510b1df48b6e85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 69e768ecb0be2645c1848a99a3b6f541a0a37dbcfb7383d96a5cc90c636fe792 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help 0 3290aebfaa3f6c4152c17a761f453ad2d79ad28147c4faf89a83a3832decee3d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -expand 0 200460ffdd5cd804a5c987d000844a91637776fc88c42cfcbb3f7888c63c0501 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +expand 0 390c1e231077bd4b61538d0f1203aae9d91220aac7f0fade8ce8ed64148a2ac7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 69470d30c1abfb98cebbabf5d62f502822cd8a0cb44dee8de42b2ba3a6349ec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 1f3efff7030b4a0a13e44736476bb920c3ae911559dd940b728e8ef5d985dc82 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 uses 0 e8eac2b7d6798ad1166bb3f6d767436ca4689c0b26f8555e147380ea8e0615a5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/routecheck.sh b/test/routecheck.sh index 38c2095b0..dadb163aa 100755 --- a/test/routecheck.sh +++ b/test/routecheck.sh @@ -11,10 +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. -# RE-PIN 2026-09-13 (lane/sc-legend, PR #215): routefix/golden_for.xml 3,776 -> 3,805 B (+29 B, est_tokens -# 1433 -> 1445). ONE identified change: the sc= composition rule joins the legend. The golden is the --no-route -# capture, so the route= code's reading is NOT here -- the control on that clause being present-only. Verified -# before re-pinning: with that clause and est_tokens= normalised out, live and previous goldens are byte-identical. +# 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. diff --git a/test/routefix/golden_for.xml b/test/routefix/golden_for.xml index 7c97289b6..48407f2b1 100644 --- a/test/routefix/golden_for.xml +++ b/test/routefix/golden_for.xml @@ -1,4 +1,4 @@ -A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall()A file whose prose describes how resolution works — a conceptual "how does resolution work" query should find this via subtoken+body matching ("resolution", "resolve"), not by any single whole-nameint lookupDefinition( int name )resolveSymbol — the resolution subsystem's entry point; the conceptual-query gate ("how does resolution work") should route to subtoken+body BM25 and can surface this by its "resolution" body vocabuint resolveSymbol( int id )how the resolver resolves a call: it resolves the callee name, then resolves overloads by arityvoid resolveCall() Date: Sun, 13 Sep 2026 15:27:58 -0400 Subject: [PATCH 14/22] fix(json,docs): a presence rule that said ONE place and still asked twice, and three published byte counts that stopped reproducing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three defects the merged tree surfaced, each measured on this build. THE PRESENCE RULE WAS STILL OPEN-CODED AT THE ONE SITE THE COMMIT CLAIMED IT HAD CLOSED. serialize.h introduced hasScopeAttr() so five readers ask one question; jsonSigRowHead kept its own `if( !s.scope.empty() )` around a nested `if( hasScopeAttr( s ) )` — behaviourally identical, and exactly the shape the helper exists to remove: two conditions to keep in step where the comment above them promises one. A rule stated in a comment and not applied by the code is not a rule. One condition now. test/printf_parity.manifest: help_all re-measured against the MERGED --help text. The merge resolved `help` to main's side (#217's re-worded --replace-symbol-body summary) and `expand` to this lane's (the whole-file serving's sc=), but help_all had moved on BOTH sides, so neither parent's hash describes the text that ships. Measured here: 5a018078 -> 87024074, UPDATE_GOLDEN_EXPECT={help_all} matched, 41 labels unchanged, and the gate reads ALL PASS over 42 verbs. CHANGELOG NUMBERS RE-MEASURED, because three of them no longer reproduced on the merged tree and a number that does not reproduce is not evidence: * the per-call compact deltas are re-read at the merge of origin/main 0e3573af — `pagerank power iteration` 9,875 -> 9,338 B, `rank graph teleport` 10,121 -> 9,812, `escapeXml` 6,290 -> 4,918. The three DELTAS (-537, -309, -1,372) and the row counts (25 -> 29, 22 -> 25) are the claim; the entry now says so, because the absolutes carry churn=/amp= readings derived from git history and move by a few bytes with every commit landed. * `est_tokens="1149"` at --token-budget=1200 reads 1146 here, same cause. Still comfortably inside the budget with every clause riding, which is the finding; the number is the number this build prints. * the manifest paragraph now states what each of the lane's two merges with main moved, and which side each label was resolved to. Gates: printffmtparitycheck ALL PASS (42 verbs); gatecount_build --check and limits_build --check both clean (614 gates, 210 caps). Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 22 ++++++++++++++-------- src/serialize.h | 4 ++-- src/taskroute.h | 2 +- test/printf_parity.manifest | 2 +- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f06bf3d8..3249c8e91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -69,10 +69,12 @@ here, pinned at **670** as the new `ripwire.for/v1` row (the table's own rule: t 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 on this repository, default legend → `--legend=compact`, three -tasks measured with `wc -c` on this build: `pagerank power iteration` 9,881 → 9,344 B (−537, and 25 → 29 -signature rows, because the bytes the legend gives back are spent on rows), `rank graph teleport` -10,127 → 9,818 B (−309, 22 → 25 rows), and the name-exact `escapeXml` 5,823 → 4,601 B (−1,222). The MCP +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. @@ -127,9 +129,13 @@ because A1′'s present-only `--for` legend outweighs what both lanes added); th `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 the merge (`UPDATE_GOLDEN_EXPECT` matched, 41 labels unchanged); across -the whole lane seven of its 42 labels moved — six for the row-6 row shape and `help_all` once more for the -merged `--help` text. +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 @@ -199,7 +205,7 @@ against, and dropped legend clauses from documents its own root reports as confo 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="1149"` +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. Rung zero also stopped being byte-negative. It removed 110–164 bytes of clauses and spliced a 161-byte diff --git a/src/serialize.h b/src/serialize.h index 3a8b7ae03..40cd88a12 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -7537,9 +7537,9 @@ inline std::string jsonSigRowHead( const IngestResult& ing, NodeId id, std::uint // P2.3: the chain key — "n" always, "id" only when the canonical form adds an enclosing scope // (the XML sibling's rule, sigRowHead above), so a JSON consumer can chain onward too. appendJsonStrField( head, ",\"n\":", s.name ); - if( !s.scope.empty() ) // row 6: the XML sibling's sc= — keys mirror attribute names one to one + if( hasScopeAttr( s ) ) // row 6: the XML sibling's sc=, ONE presence rule — keys mirror attribute names one to one { - if( hasScopeAttr( s ) ) { appendJsonStrField( head, ",\"sc\":", s.scope ); } // row 6: one presence rule (hasScopeAttr) + 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 60a5eeec1..972c02c75 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -918,7 +918,7 @@ inline std::optional directTaskChoice( std::string_view task, std:: // 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. // -// WHAT DECIDES: rw::legendCompactAppliesTo (cli.h), the SAME list of non-XML surfaces the binary REFUSES the flag +// 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. diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index c6568c3c8..9e579a7cd 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -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 5a018078702cd7b30fa47053bae3ef0c3d332b3a4e69f58657ac924ba2c6cd4b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 87024074d340a6ac2d44f3a70db5c99d037446490e042517ab24b1bd86182429 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From aba401615abde2271b343949b723a4569e461ec1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 15:32:16 -0400 Subject: [PATCH 15/22] =?UTF-8?q?chore(quality):=20ack=20the=20one=20gatin?= =?UTF-8?q?g=20row=20=E2=80=94=20the=20parameter=20that=20makes=20the=20co?= =?UTF-8?q?mpact=20route=3D=20clause=20present-only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--quality-delta=origin/main..HEAD` gated on one preexisting-worse major row: api-surface appendCompactForLegend, was=3 now=4 (src/verbs_for.h:765). It is a genuine consequence, not drift: 761c45b8 gave the compact dialect's legend writer `withRouteAttr` so its route= reading rides only when the root carries the attribute — the same present-only condition the default dialect has always used, and the parameter IS that rule. The function is a file-internal inline helper with two callers in one translation unit; no published contract moved. Acked through the binary, by symbol, --ack-only=contract-change. The ledger diff adds exactly one line (+ack api-surface f3ea5dbe8b5720cb) and nothing else; the binary reports "acknowledged 1 of 13 (12 left UNACKED by --ack-only, 7 already acked)". After it: regressions=12, acked=8, preexisting-worse=8, new-symbol=4, minor=8, gating="0", exit 0. The eleven non-gating rows were read, not waved through. Four are new symbols this round created and named (classifyRoutes, the one router exit that applies the compact posture; compactForNote; the forCoverageAttrPresent / forRouteAttrPresent pair the duplication row sees as 24 shared tokens — two one-line find() predicates whose whole purpose is that each question has ONE site). Seven are minor: climbCeilingLadder/climbCeilingLadderBy at 6 and 7 params, which is the two-ceiling ladder review item 1 asked for; and the verbosity/complexity ticks on applyCompactDialect, runDefaultMap, kHelpHead and the two compactlegend tables, each of them the bytes of the expand-file schema and the whole-file legend. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 1 + 1 file changed, 1 insertion(+) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 0b5e8c75e..2b26950e6 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -142,6 +142,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 From a89767ebf95dd5912900790940d1cb0418a11928 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 15:59:27 -0400 Subject: [PATCH 16/22] test(fixedbuf,format): the full suite caught two the lane's targeted set never ran MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FULL SUITE, foreground, 628 gates. Two reds the review round created and no targeted set would have shown — the rule this repo added on 2026-09-13 after every lane's targeted set missed 2-15 gates on CI, earning its keep on the first run under it. (1) fixedbufsweep (S2)/(S6): src/serialize.h buffer 'lb' — the TABLE said 3 call sites, the source has 4. Item 8 made MergedCalleeNameRow carry its lines as uint32 values sorted ascending at append time instead of accumulating them as text in walk order (so one fact stopped having two spellings between queries), which moved the digit formatting into appendMergedCalleeNameRows and gave that name a fourth buffer in that file. Re-derived from `git diff 6e8dd75a -- src/`, not accepted from the gate's suggested delta, because that is exactly how this gate's own header records it rotting: the new site is `"{}"` of one std::uint32_t — no %s, nothing escaped, ten digits worst case against 15 usable + NUL — so it joins the EXISTING ( serialize.h, lb ) row (3 -> 4) and NOT the string-interpolating population. rows and widthforms are therefore unmoved, which is the check that the reading is right: calls 218 -> 219, mentions 322 -> 323, sites 218 -> 219, rows 92, widthforms 0. Green: fixedbufsweep ALL PASS. (2) formatgatecheck: src/graphlegend.h is one of the nine files gated to match .clang-format byte for byte, and ForIdRouteLegendParts was written in the house comment alignment the rest of the tree uses. For a GATED file the formatter decides, so it was run: four lines, comment spacing and one ternary, no semantic change. Green: formatcheck ALL PASS (clang-format 22, 9 gated files). The suite's other two reds are not defects: strkerncheck timed out at its declared 300 s budget under six-way parallelism with another worktree building on the same machine (it passes re-run alone), and the freshness gate read built_from=a0a6ef355 against a HEAD that had moved one ack commit further — both re-run against this commit's binary. Co-Authored-By: Claude Fable 5.1 --- src/graphlegend.h | 8 ++++---- test/fixedbufsweep.sh | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index 19fabf248..fa2464bc2 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -311,8 +311,8 @@ inline constexpr std::string_view kForRouteCodeLegend = // once and uses `.sc`/`.route` to append and `.bytes()` to exempt, so the two can only ever agree. struct ForIdRouteLegendParts { - std::string_view sc; // "" when no served row carries a scope - std::string_view route; // "" when this root carries no route= + std::string_view sc; // "" when no served row carries a scope + std::string_view route; // "" when this root carries no route= std::size_t bytes() const noexcept { return sc.size() + route.size(); } }; @@ -321,9 +321,9 @@ inline ForIdRouteLegendParts forIdRouteLegendParts( bool legendOn, bool scPresen { if( !legendOn ) { - return {}; // rung zero took the readings; the attributes stay and the dropped note names them + return {}; // rung zero took the readings; the attributes stay and the dropped note names them } - return { scPresent ? kForIdRouteLegend : std::string_view(), + return { scPresent ? kForIdRouteLegend : std::string_view(), routePresent ? kForRouteCodeLegend : std::string_view() }; } diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index d74a7e741..5dc001c6d 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -248,7 +248,12 @@ NUMERIC_ONLY = { ( "src/serialize.h", "gfb" ): 1, ( "src/serialize.h", "inAttr" ): 1, ( "src/serialize.h", "kbuf" ): 1, - ( "src/serialize.h", "lb" ): 3, # row 6 (2026-09-12): collectCalleeNameRow's line buffer (the merged row) joined the two + ( "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" ): 1, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge ( "src/serialize.h", "precAttr" ): 1, @@ -431,7 +436,16 @@ 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. -EXPECTED = { "mentions": 322, "calls": 218, "sites": 218, "rows": 92, "widthforms": 0 } +# 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. +EXPECTED = { "mentions": 323, "calls": 219, "sites": 219, "rows": 92, "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, From 3944f5919608ff0bc42b9cf14561e16b7f78946c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 17:49:36 -0400 Subject: [PATCH 17/22] fix(est,pincensus): the byte model still charged the id= the row stopped printing, and a gate stopped reading the path it was about MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four items from CodeRabbit review 5191303552, three in the diff and one outside it. Each reproduced before it was touched. 1. THE MODEL CHARGED A SHAPE THE ROW NO LONGER PRINTS (src/serialize.h, estimateTokens, outside this lane's diff). Row 6 replaced the row's id="PATH::SCOPE::NAME" with ` sc="SCOPE"`, and the byte model was not moved with it: per scoped symbol it still added `path + scope + name + 4`. The path is already charged once per file at `seen[f]` and the name once on the row just above, so every scoped row billed BOTH a second time. Measured on this repository's flagless map: 137 scoped rows, 4,129 B of double charge. Over-charging is not the safe direction. mapEstTokens is what the T3 fill-order auto-flip compares against kFillOrderThreshold and what both open_memstream degrade paths report as est_tokens=; and the model's RATE (bytesPerToken(), modelBytes/tokens) is what every normal-path est_tokens= divides the emitted bytes by (tokensForEmittedBytes), which is in turn what --token-budget withholds a map on as withheld_est_tokens=. So a model pricing a shape the emitter does not print moves a threshold, a degrade reading and a published rate at once. Charged at what the row prints: 6 B of markup for ` sc=""` plus the scope segment. The presence test is now hasScopeAttr(), the same predicate the four emitters use — forward-declared here, because a model that re-derives the emitter's rule is exactly how it came to charge a retired one. MEASURED, est_tokens before -> after: this repository 9,049 -> 9,033; test/fixture 894 -> 895; test/cppqualfix 1,119 -> 1,117; test/nestedqualfix 780 -> 777; test/pincensusfix 917 -> 905. The signs differ because the reported number is emitted-bytes / the model's own rate: removing content bytes priced at a language rate changes the weighted average, and on this repository it moved 2.476 -> 2.481 B/tok. No emitted document changed by one byte. PINS: test/fillordercheck.sh's test/fixture est_tokens 894 -> 895, and test/golden.xml regenerated. Both are the SAME 2,234 B document as before — `est_tokens=` in the stats preamble is the only character that differs, verified by diffing with est_tokens normalised out before the golden was rewritten. test/estchargecheck.sh is green untouched, which is the calibration check: all eight of its charge ratios stay inside their bands (map 2.48, metrics 2.48, sigs 2.49, src 3.71, bodies 3.29, outline 2.78, bodies+sigs 2.87, maxtokens 2.49). 2. A GATE STOPPED PROVING THE THING IT IS ABOUT (test/pincensuscheck.sh). Arm (A) was re-keyed off the retired id= onto `n=` plus `sc=` over the WHOLE map, so it no longer asserted that Alpha::run belongs to pinned.py or Eps::go to tied.py — the locality pin is a claim about a FILE, and the arm had stopped reading the file half of it. It passed by luck: Alpha exists only in pinned.py and Eps only in tied.py on this fixture, so nothing else could match. The canonical id composes as p::sc::n with p= from the enclosing , so the lookup composes it the same way (fileBlock/fileScopedRow, index() throughout so a '.' in a path is a '.'): the row must sit inside THAT file's block and carry THAT sc= and THAT n=, and resolve to exactly one row. The edge walk starts from that file-bound row too. RED FIRST, two mutants: (a) claiming Alpha::run under p="tied.py" fails with "does not resolve to one row inside "; (b) reverting the lookup to the old whole-map grep fails BOTH new control arms ("matched under the WRONG path — this lookup is not reading p="). Mutant (b) is the one that matters: it is the shape that shipped, and it is now red. 3. docs/COMMANDS.md's routed --for example: already correct as of 2bdad93a's ref-clean regeneration — the row reads with no id=. The one canonical id left in that file is --uses's in_id=, which names a DIFFERENT symbol than its row. 4. skills/ripwire-router/SKILL.md documented only the --for exemption, as though the compact posture were all choice. The binary REFUSES the flag on every command that has no XML legend to compact, and a refused command does not run — which is how `--zoom --legend=compact --mermaid` shipped. The convention section now names both families it refuses, the state-changers (--quality-ack, --quality-baseline, --arch --baseline, --note-add, the three symbol writers, --edit-plan, --index-out=, --pin-census=) and the non-XML renderings, and says the thing that actually prevents the mistake: a flag that turns an XML verb into one of these takes the whole command with it, so the verb does not decide, the command does. skilltruthcheck still reads 43 distinct commands, all accepted. Co-Authored-By: Claude Fable 5.1 --- skills/ripwire-router/SKILL.md | 4 ++- src/serialize.h | 24 ++++++++++++++--- test/fillordercheck.sh | 9 ++++++- test/golden.xml | 2 +- test/pincensuscheck.sh | 49 +++++++++++++++++++++++++++++++--- 5 files changed, 77 insertions(+), 11 deletions(-) diff --git a/skills/ripwire-router/SKILL.md b/skills/ripwire-router/SKILL.md index e32302b92..4b870ecca 100644 --- a/skills/ripwire-router/SKILL.md +++ b/skills/ripwire-router/SKILL.md @@ -84,7 +84,9 @@ a rule an agent must remember to look up is a rule that loses to a habit. ## The legend: compact by default, full when a definition needs its reasoning -Every command these skills spell asks for the compact legend (`--legend=compact`: terse definitions of only the attributes the answer carries). Add `--legend=full` when a definition's reasoning is needed — a term you do not recognise, a floor or cap you need explained, or a map a human will read. `--for` is the exception: its default legend is already the one to read first, and its compact legend is its own. +Every command these skills spell asks for the compact legend (`--legend=compact`: terse definitions of only the attributes the answer carries). Add `--legend=full` when a definition's reasoning is needed — a term you do not recognise, a floor or cap you need explained, or a map a human will read. `--for` is the exception by CHOICE: its default legend is already the one to read first, and its compact legend is its own. + +The other exceptions are not a choice — **the binary refuses the flag on any command that does not answer with an XML legend**, and a command it refuses runs not at all. Two families: the **state-changing** commands, which write something rather than answer (`--quality-ack`, `--quality-baseline`, `--arch --baseline`, `--note-add`, `--replace-symbol-body` / `--insert-before-symbol` / `--insert-after-symbol`, `--edit-plan`, `--index-out=`, `--pin-census=`), and the **non-XML renderings** (`--mermaid`, `--html`, `--report`, `--sarif`, `--situ`, `--recall=`, `--export`). Those carry no `--legend=` at all. This matters when you compose rather than copy: a flag that turns an XML verb into one of these — `--zoom --mermaid`, `--quality-delta --quality-ack` — takes the whole command with it, so the verb is not what decides, the command is. Nothing here is a rule you have to apply by hand: `ripwire --help-task` applies it for you, and `test/skilltruthcheck.sh` runs every `--legend=compact` command these skills spell against the binary and fails on a refusal. ## Cross-cutting disciplines (fire ALONGSIDE a moment skill, not instead) diff --git a/src/serialize.h b/src/serialize.h index 40cd88a12..b7c93ec2f 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1385,7 +1385,7 @@ inline constexpr std::size_t kEnvelopeBytes = 320; // Per-element MARKUP byte costs (default map), measured against real output: // = 12 + path (+9 when a builtin layer= tag is present) -// = 19 + name (+11 for k=, +6+canon when scoped; metrics adds more) +// = 19 + name (+11 for k=, +6+scope when scoped; metrics adds more) // = 9 + callee-name inline constexpr std::size_t kFileMarkupBytes = 12; inline constexpr std::size_t kSymMarkupBytes = 19 + 11; // base tags + the default k="0.XXXX" attr @@ -1419,6 +1419,12 @@ inline constexpr std::size_t kFillOrderThreshold = kNominalWindowTokens / 2; // ORDER, so it cannot wait for the emitted bytes), which is precisely what a pure function of the symbol // set is for; the REPORTED size describes the finished document and is measured. Both are documented at // their use sites in serialize(). +// The sc= presence rule, declared here and defined beside writeScopeAttr below: the byte MODEL must charge a +// scope attribute on exactly the rows the emitter prints one on, so it asks the same question the emitters do +// rather than re-deriving it — that re-derivation is how the model came to charge a shape the row stopped +// printing (PR #215 review). +inline bool hasScopeAttr( const Symbol& s ) noexcept; + inline TokenEstimate estimateTokens( const IngestResult& ing, const std::vector& order, std::size_t keep, const std::vector& outOff, const std::vector& outTargets ) { @@ -1440,10 +1446,20 @@ inline TokenEstimate estimateTokens( const IngestResult& ing, const std::vector< } markupBytes += kSymMarkupBytes; contentBytesByLang[ li ] += double( s.name.size() ); - if( !s.scope.empty() ) - { + if( hasScopeAttr( s ) ) + { + // ROW 6, AND THE MODEL FOLLOWED IT LAST (PR #215 review, CodeRabbit 5191303552). The row used to + // print id="PATH::SCOPE::NAME", and this charged exactly that: the file path, the scope, the name + // and the two "::" separators. The row prints ` sc="SCOPE"` now — the path once per (charged + // above, at `seen[f]`) and the name once on the row (charged just above) — so the path and the name + // were being billed a SECOND time on every scoped symbol. Over-charging is not the safe direction: + // mapEstTokens is what `--token-budget` withholds a map on (main.cpp, `withheld_est_tokens=`), what + // the open_memstream degrade path reports as est_tokens=, and what the T3 fill-order auto-flip + // compares against kFillOrderThreshold — so an inflated model withholds maps that fit and flips an + // order that should not have flipped. Charged at what the row prints: 6 B of markup for ` sc=""` + // and the scope segment as content, nothing else. markupBytes += 6; - contentBytesByLang[ li ] += double( ing.files[f].size() + s.scope.size() + s.name.size() + 4 ); + contentBytesByLang[ li ] += double( s.scope.size() ); } for( std::uint32_t e = outOff[id]; e < outOff[id + 1]; ++e ) { diff --git a/test/fillordercheck.sh b/test/fillordercheck.sh index 079adfd20..b479cf2dd 100755 --- a/test/fillordercheck.sh +++ b/test/fillordercheck.sh @@ -115,7 +115,14 @@ OFIX="$( order_of test/fixture )" # 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. -{ [ "$EFIX" = "894" ] && [ "$OFIX" = "important-first" ]; } \ +# 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/golden.xml b/test/golden.xml index 0a03e5de3..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/pincensuscheck.sh b/test/pincensuscheck.sh index 38595aac7..d7f679d80 100755 --- a/test/pincensuscheck.sh +++ b/test/pincensuscheck.sh @@ -49,15 +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 )" -# row 6 (2026-09-12): a scoped row prints n= then sc= (the short id); the canonical id composes as ::sc::n -RUN_ROW="$( printf '%s' "$MAP" | tr '<' '\n' | grep 'n="run" sc="Alpha"' )" -GO_ROW="$( printf '%s' "$MAP" | tr '<' '\n' | grep 'n="go" sc="Eps"' )" + +# ── 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 '/n="run" sc="Alpha"/{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\"" \ From 6130e3f59c2fe316e3dc4a78c9ccda77bd836330 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 18:16:48 -0400 Subject: [PATCH 18/22] fix(expand,legend): a legend chose its schema by grepping the whole document, and a candidate was priced without the bytes only it pays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four more findings from the second CodeRabbit pass, each reproduced first. Two are bugs THIS LANE introduced; one is a comment that did not describe its own code; one is a rendering break. A fifth is confirmed VALID and deliberately NOT fixed here — see the last section. 1. THE LEGEND SELECTOR READ THE WHOLE DOCUMENT (src/compactlegend.h). --expand has two servings and two compact schemas, and applyCompactDialect chose between them with `doc.find( "mode=\"whole-file\"" )` over the ENTIRE document. An --expand bundle carries its bodies as CDATA, so any body that merely MENTIONS that literal took the expand-file legend onto a document made of // — a legend describing a shape the document does not contain, which is the exact defect the expand-file schema was ADDED to fix, pointed the other way. This repository can trigger it: test/scroundtripcheck.sh greps for that literal. REPRODUCED on a 400-function Python fixture whose first body holds the string: the root printed mode="bundle" reason="bundle 1957B <= file 11844B" and served , under schema="ripwire.expand-file/v1". The serving mode is a ROOT ATTRIBUTE and is read only there now. RED FIRST: the new scroundtripcheck (E3) arm fails on the archived pre-fix binary ("took the WRONG legend: schema=ripwire.expand-file/v1 (root says mode=bundle)") and passes on this one, with a non-vacuity arm proving the probe body really carries the literal and a positive control proving a REAL whole-file serving still selects expand-file. 2. A CANDIDATE PRICED WITHOUT THE BYTES ONLY IT PAYS (src/main.cpp). chooseExpandServe compares a bundleBytes priced to the byte — root attrs, unproven residue, map, bodies section — against wf.rawBytes, but the whole-file legend this lane added is appended AFTER the decision and only in that mode. The file side was under-priced by exactly 236 B, so inside that band the tool chose, and REPORTED, the whole-file form as smaller while the bundle it rejected was the smaller document. The legend is one constant now, charged in the comparison and appended to the root, so the choice cannot be made on a price the document does not pay. MEASURED on test/nestedqualfix --expand=Outer: reason "file 1366B < bundle 3380B" -> "file 1602B < bundle 3380B", 1366 + 236 = 1602. The pack-budget arm stays on rawBytes: that ceiling is about the file's own size, not about which candidate is cheaper. 3. THE LADDER CONTRACT DESCRIBED A DIFFERENT LADDER (src/serialize.h). The block comment said rungs (a) and (b) are both judged by fitsExact; the code judges (b) by fitsAllowance, and the code is right. The asymmetry is the design: DROPPING the echo costs a reader nothing, so it is tried at the tighter number, but REFUSING it for a residual inside the tolerance would send the ladder on to (c), which throws route= away — real, unique information — to buy bytes the tolerance already grants. A bundle can stop at (b), keep route=, and still be labelled over_ceiling="1"; that is the tolerance working, not a missed rung. The comment now says so, including the consequence, so the next reader does not file it again. 4. skills/ripwire-mcp/SKILL.md: an unescaped `|` inside inline code split a 3-column table row into 4 cells, dropping everything after `subtoken+body[:broad` from the rendered table. Escaped; the row parses as 3 cells like its header. NOT FIXED HERE, AND DISCLOSED RATHER THAN CARRIED QUIETLY. The reviewer also flagged that --for's fitsExactCeiling compares raw bytes against ceilingBytes(budget) while est_tokens prices bodies at kBytesPerTokenBody (3.80) and markup at kBytesPerTokenDefault (2.50). The finding is VALID — rung zero still fires on documents that fit — though the mechanism is not only the body rate: `ripwire . --for="pagerank power iteration" --token-budget=2500` delivers 5,769 B against the 6,250 B its root promises, with bodies="0", no over_ceiling=, 481 B of headroom, and has dropped its clauses anyway. So the ladder is charging a payload the finished document does not carry, and the earlier fix in this lane corrected the RATE but not the BYTES. That is the ceiling ladder, the most load-bearing code in this lane, and a rushed second fix to it at the end of a round is how a pin table gets re-measured twice. It gets its own round. The CHANGELOG entry for the original fix now states the residual with this measurement instead of claiming the defect closed, because a fix written up as complete when it is partial is worth less than the number. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 10 ++++++++++ skills/ripwire-mcp/SKILL.md | 2 +- src/compactlegend.h | 16 +++++++++++++--- src/main.cpp | 27 +++++++++++++++++++++------ src/serialize.h | 14 ++++++++++---- test/printf_parity.manifest | 4 ++-- test/scroundtripcheck.sh | 29 +++++++++++++++++++++++++++++ 7 files changed, 86 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 008b21907..a15bfa45c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -256,6 +256,16 @@ aim at the exact ceiling, while dropping `route=` and labelling the bundle keep 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. +**Not fully closed, and measured rather than assumed.** The same rung still fires on some documents that fit: +`ripwire . --for="pagerank power iteration" --token-budget=2500` delivers 5,769 B against the 6,250 B its root +promises — 481 B of headroom, `bodies="0"`, no `over_ceiling=` — and has still dropped its legend clauses and +spliced the 193-byte note. So `fitsExactCeiling` is rejecting documents that fit: the rate is right now, the +bytes it prices are not (the ladder charges a payload the finished document does not carry). The remaining +error is a fraction of the original one and in the same direction — a lens trimming an explanation it could +have afforded, never a document over its budget — and it is stated here rather than left for a reader to find, +because a fix that closes most of a defect and is written up as closing all of it is worth less than the +measurement. The residual is its own round. + 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, diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index d72af6b8f..d1acbb69f 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -90,7 +90,7 @@ enclosing-chain report) and `@FILE:LINE` in any SYM selector; contract gate: `te | Verb | CLI twin | Ask it for | |---|---|---| | `analyze` | `ripwire

` | the ranked XML map | -| `for` (`task`) | `--for=TASK` | the task lens: signatures + cx/in metrics framed for reuse. **Auto-routes** the ranker — pass a symbol NAME verbatim as `task` to get name-exact retrieval (recall@1 ~99%); a conceptual phrase uses subtoken+body. Root carries `route=` as a code (`name-exact(X)` / `subtoken+body[:broad|:declined]`). | +| `for` (`task`) | `--for=TASK` | the task lens: signatures + cx/in metrics framed for reuse. **Auto-routes** the ranker — pass a symbol NAME verbatim as `task` to get name-exact retrieval (recall@1 ~99%); a conceptual phrase uses subtoken+body. Root carries `route=` as a code (`name-exact(X)` / `subtoken+body[:broad\|:declined]`). | | `find_symbol` (`symbol`) | `--callers` + `--callees` | locate a symbol with its callers AND callees in one call — each symbol carries a `handle` | | `find_referencing_symbols` (`symbol`) | `--callers=SYM` | just who references/calls it — also handle-bearing | | `grep` (`pattern`) | `--grep=STR` | parallel literal scan + enclosing symbol + matched line | diff --git a/src/compactlegend.h b/src/compactlegend.h index 241060e71..4caf081b3 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -870,9 +870,19 @@ inline CompactOutcome applyCompactDialect( std::string& doc, std::string_view hi { const CompactRootInfo root = findCompactRoot( doc ); if( root.tag.empty() ) { return CompactOutcome::NotXml; } - // …and the root says WHICH serving this is, for the one verb that has two (see the expand rows above). - std::string_view effectiveHint = hint; - if( hint == "expand" && doc.find( "mode=\"whole-file\"" ) != std::string::npos ) + // …and the ROOT says which serving this is, for the one verb that has two (see the expand rows above). + // + // THE ROOT TAG, NOT THE DOCUMENT (CodeRabbit 5216..., PR #215). This searched the WHOLE document, and an + // --expand BUNDLE carries its bodies as CDATA: any body that merely MENTIONS the literal `mode="whole-file"` + // — a gate script that greps for it, a doc that quotes it, this repository's own test/scroundtripcheck.sh — + // selected the expand-file legend for a document made of //. REPRODUCED on a 400-function + // Python fixture whose first body holds the string: the root printed mode="bundle" reason="bundle 1957B <= + // file 11844B" and served , under a legend reading `; `. That is the very + // defect the expand-file row exists to fix, pointed the other way — a legend describing a shape the document + // does not contain. The serving mode is a ROOT ATTRIBUTE and is read only there. + std::string_view effectiveHint = hint; + const std::string_view rootOpen = std::string_view( doc ).substr( root.openBegin, root.openEnd - root.openBegin ); + if( hint == "expand" && rootOpen.find( " mode=\"whole-file\"" ) != std::string_view::npos ) { effectiveHint = "expand-file"; } diff --git a/src/main.cpp b/src/main.cpp index e19d59311..6a76bce1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1125,25 +1125,42 @@ struct ExpandServeChoice std::string ctxOpen; }; +// The whole-file serving's own legend (PR #215 review item 9), as ONE constant: the bytes CHARGED in the +// bundle-vs-file comparison below and the bytes APPENDED to the root in runDefaultMap are the same object, so +// the choice cannot be made on a price the document does not pay. It carries no "--": it rides inside an XML +// comment, where a double hyphen is ill-formed (G4). +inline constexpr std::string_view kExpandWholeFileLegend = + ""; + inline ExpandServeChoice chooseExpandServe( std::size_t bundleBytes, const rw::WholeFileRender& wf, std::size_t budgetBytes ) { char open[ 160 ]; ExpandServeChoice c; + // WHAT THE FILE CANDIDATE ACTUALLY COSTS (CodeRabbit, PR #215). This compared wf.rawBytes against a + // bundleBytes that is priced to the byte — root attrs, the unproven residue, the map, the bodies section — + // while the whole-file candidate's own legend is appended AFTER this decision and only in that mode. The + // file side was therefore under-priced by exactly this constant, so inside that band the tool chose, and + // REPORTED, the whole-file form as smaller while the bundle it rejected was the smaller document. The + // pack-budget arm above is deliberately left on rawBytes: that ceiling is about the FILE's own size + // against --pack-budget, not about which of two candidates is cheaper to serve. + const std::size_t wholeFileBytes = wf.rawBytes + kExpandWholeFileLegend.size(); if( wf.complete && wf.rawBytes > budgetBytes ) { rw::formatTo( open, sizeof( open ), "", wf.rawBytes, budgetBytes ); } - else if( wf.complete && wf.rawBytes < bundleBytes ) + else if( wf.complete && wholeFileBytes < bundleBytes ) { c.serveWholeFile = true; rw::formatTo( open, sizeof( open ), "", - wf.rawBytes, bundleBytes ); + wholeFileBytes, bundleBytes ); } else if( wf.complete ) { rw::formatTo( open, sizeof( open ), "", - bundleBytes, wf.rawBytes ); + bundleBytes, wholeFileBytes ); } else { @@ -1851,9 +1868,7 @@ int runDefaultMap( const MainDispatch& d ) // anywhere: it rides inside an XML comment, where a double hyphen is ill-formed (G4). if( serveWholeFile ) { - ctxOpenStr += ""; + 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 diff --git a/src/serialize.h b/src/serialize.h index b7c93ec2f..7c1475799 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -757,10 +757,16 @@ struct CeilingLadderChoice // TWO CEILINGS, ONE LADDER (2026-09-13, PR #215 review item 1). `fitsExact` is the ceiling the root PROMISES // (ceilingBytes: est_tokens <= budget_tokens); `fitsAllowance` is that ceiling plus the first-entry tolerance. // Which rung is judged by which is the whole design: -// (a) as built and (b) echo dropped are judged by fitsExact — the echo is a byte-for-byte duplicate of task=, -// so spending it costs the reader NOTHING, and a document whose own root will say over_ceiling="1" should -// spend every free rung before it says so. Rung zero (the caller's droppable legend clauses, above this -// function in --for) belongs to the same class and uses the same ceiling. +// (a) as built is judged by fitsExact — a document that already fits what its root PROMISES keeps everything, +// and rung zero (the caller's droppable legend clauses, above this function in --for) is entered on the +// same ceiling for the same reason: both are free, so they are tried at the tighter number. +// (b) echo dropped is TRIED because (a) failed the exact ceiling, and ACCEPTED at the allowance. The two are +// not the same question and the asymmetry is deliberate: the echo is a byte-for-byte duplicate of task=, +// so DROPPING it costs the reader nothing and is worth doing at the tighter number; but REFUSING it for a +// residual inside the tolerance would send the ladder on to (c), which throws route= away — real, unique +// information — to buy bytes the tolerance already grants. Accepting (b) at the allowance is what keeps +// (c) from firing on an overshoot (c) exists to tolerate. A bundle can therefore stop at (b), keep route=, +// and still be labelled over_ceiling="1" by the verdict: that is the tolerance working, not a missed rung. // (c) route= dropped and (d) the honest label are judged by fitsAllowance — (c) is the first UNIQUE-information // loss and (d) is the verdict, and the tolerance exists precisely so neither fires on a residual a lens // cannot trim. Trimming real content, or calling a lens failed, at the exact ceiling would spend the diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 9e579a7cd..84c1ca476 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -1,4 +1,4 @@ -flagless 0 e7a5fbb7a683fa5edb35b38695d9ff1f027cd28401881f1bef9391754e87dd79 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +flagless 0 0553422259c2f203b0819ab4bb8a769d31b244360a6e7d81851ab5dbf0591e1c e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint 0 bd805656a4ccad157cbac04128866b4d2eecdaec891a5b0d3fd6af6118f77256 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_sarif 0 805e375b06b7755bcc6423607378e557ee82fef787cf09c34ba5c066639f4d7b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_select 0 156ac3dbd4bd5d184e7182ce412ec57dd1886fadc76c0df7378dd51e556f9b33 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -17,7 +17,7 @@ uses 0 e8eac2b7d6798ad1166bb3f6d767436ca4689c0b26f8555e147380ea8e0615a5 e3b0c442 path 0 7058d89f7bab7aabe0a5cbf921959bc8a2c346fe65888b776996f879f04a58a2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connect 0 31c7e3a689a6dcddf5eae17283740823005c3efdfdf3945be5770b2660a152b7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 b171aa1e5c28b47827f9148c2a5fc6948fb22938d3da21e6551b12432f492472 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_signatures 0 cd9324abaee039376e3e19981d56cd83cf9a758741b3bfd073575379daea0f6d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_signatures 0 0d79e8f31f99dc1936a25b016b447ea8dcae12cad984067e6f2e3d70378a61ae e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_task 0 519b456fa804af76cb8011937c7e56a7542b0ad84a2ff57f54b442e853d815f7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 6014e2f18ba2d587f70d59fcbe62f64c38e8c556291006ae3dfd76ef7259315a e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 23b824f67984709f3c01dc7cc97c4c682b2afbaa5b63677a79ce56a1a58e6454 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/scroundtripcheck.sh b/test/scroundtripcheck.sh index a1f95b3fd..f824e7e3c 100755 --- a/test/scroundtripcheck.sh +++ b/test/scroundtripcheck.sh @@ -174,4 +174,33 @@ grep -q ']*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 Date: Sun, 13 Sep 2026 18:54:57 -0400 Subject: [PATCH 19/22] test(printf-parity): re-pin expand for the file price it now states The whole-file candidate is charged its own legend before chooseExpandServe compares it, so the reason= it prints states what that candidate actually costs. MEASURED against an archived build of the parent commit, `--expand=distance` on test/fixture: reason="file 701B < bundle 3702B" -> "file 937B < bundle 3702B", and 701 + 236 = 937 exactly, where 236 B is kExpandWholeFileLegend. The document is the SAME 1,153 bytes before and after, the same mode="whole-file", the same est_tokens="303"; that number is the only thing in it that differs. UPDATE_GOLDEN_EXPECT={expand} matched, 41 labels unchanged. help and help_all did not move across the merge with #218. Co-Authored-By: Claude Fable 5.1 --- test/printf_parity.manifest | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 84c1ca476..757eae769 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -10,7 +10,7 @@ callers 0 c8317dc738f05616369995c0a0c03a6c12fdea8d2a2c5b7d182156a4de76cdc6 e3b0c impact 0 4009dd7227dabc63a96e6318e77c6f704f25b7c2a31295fa80510b1df48b6e85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 69e768ecb0be2645c1848a99a3b6f541a0a37dbcfb7383d96a5cc90c636fe792 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help 0 aee3ae7d926954108ded89973f359725523423877a55c2c1db3a5909d6740d0f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -expand 0 390c1e231077bd4b61538d0f1203aae9d91220aac7f0fade8ce8ed64148a2ac7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +expand 0 408861b7d7e0a81768cc7032ee623953dec064cd358c5dd1cee281a0e55c44aa e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 69470d30c1abfb98cebbabf5d62f502822cd8a0cb44dee8de42b2ba3a6349ec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 1f3efff7030b4a0a13e44736476bb920c3ae911559dd940b728e8ef5d985dc82 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 uses 0 e8eac2b7d6798ad1166bb3f6d767436ca4689c0b26f8555e147380ea8e0615a5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 3fb92eeee51281e822d68ef807a130f5bcce9a1b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Sun, 13 Sep 2026 20:17:46 -0400 Subject: [PATCH 20/22] fix(for): the exact ceiling was a byte test, and the bytes it counted were not the ones the document carries MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --for's ceiling ladder has a rung zero below its own three: the droppable legend trio (the confidence reading, the r=/ reading, the sc=/route= reading), dropped when the header does not fit "the ceiling the root promises". This lane fixed the RATE that test priced at (kMinBytesPerToken -> kBytesPerTokenDefault) and left the deeper half untouched, which the PR #215 review named: 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 a raw byte total against ceilingBytes( budget ) charges every body byte 1.52x what the root charges it. The same predicate also assembled its candidate from ladderPayloadBytes — a sum of RESERVES and of 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 is judged not to fit, and three definitions a budgeted reader has no other source for are spent to buy headroom that was already there. MEASURED on a git-less five-symbol fixture at --detail=1 (the body rate is the half a bodiless bundle cannot show): 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. Two more on the live tree: `. --for="ceiling ladder" --detail=6 --token-budget=1400` (kept price 1385) and `. --for="rank the symbols for a task" --token-budget=2200` (kept price 2198). THE FIX is that the exact ceiling stops being a byte test at all. finishForLensHeader is split into finishForLensHeaderPriced, which hands back the finished header AND the number it will print, and fitsExactCeiling asks the root's own question once — est_tokens <= budget_tokens, on the document that would be emitted. There is no second expression left to disagree with the first, which is how the rate drifted in the first place. The allowance predicate stays byte-based: ceilingAllowanceBytes IS a byte bound and rungs (b)/(c)/(d) are judged by it. The --max-tokens body ceiling is deliberately not folded in — widening a rung is a change to what it drops, not a fix to how it prices. GATE: test/estchargecheck.sh #18, self-anchoring rather than budget-pinned — it reads the price off a wide run where the clauses ride and probes five tokens above it, because a document that prices at E fits every budget >= E by the root's own arithmetic. RED on a git archive build of c4478402 at the probe arm ("rung zero at --token-budget=1074 dropped a legend it could afford: dropped-note=1 clauses=0/2 est_tokens=715"), GREEN after; its control (rung zero must still fire 200 tokens under E) is green on BOTH binaries, so the probe is not green for free. Full suite: gates=629 pass=627 skip=2 fail=0 — no pin moved. AND ONE CORRECTION TO THE RECORD. The residual this lane's CHANGELOG reported — `. --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 this defect and still behaves that way. The 481 B of headroom is the document AFTER the drop; the document that keeps 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. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 33 ++++++++++++----- src/serialize.h | 5 ++- src/verbs_for.h | 81 +++++++++++++++++++++++++++++++----------- test/estchargecheck.sh | 80 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index faf5a5859..458a71280 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -304,15 +304,30 @@ aim at the exact ceiling, while dropping `route=` and labelling the bundle keep 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. -**Not fully closed, and measured rather than assumed.** The same rung still fires on some documents that fit: -`ripwire . --for="pagerank power iteration" --token-budget=2500` delivers 5,769 B against the 6,250 B its root -promises — 481 B of headroom, `bodies="0"`, no `over_ceiling=` — and has still dropped its legend clauses and -spliced the 193-byte note. So `fitsExactCeiling` is rejecting documents that fit: the rate is right now, the -bytes it prices are not (the ladder charges a payload the finished document does not carry). The remaining -error is a fraction of the original one and in the same direction — a lens trimming an explanation it could -have afforded, never a document over its budget — and it is stated here rather than left for a reader to find, -because a fix that closes most of a defect and is written up as closing all of it is worth less than the -measurement. The residual is its own round. +**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 diff --git a/src/serialize.h b/src/serialize.h index 7c1475799..52805c646 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -755,7 +755,10 @@ struct CeilingLadderChoice }; // TWO CEILINGS, ONE LADDER (2026-09-13, PR #215 review item 1). `fitsExact` is the ceiling the root PROMISES -// (ceilingBytes: est_tokens <= budget_tokens); `fitsAllowance` is that ceiling plus the first-entry tolerance. +// (est_tokens <= budget_tokens); `fitsAllowance` is that ceiling plus the first-entry tolerance. Each caller +// decides how to SPELL them: the fixed-payload wrapper below has one rate and compares bytes, while --for's +// lens prices markup and bodies at different rates and so asks its exact question in TOKENS, on the finished +// document (verbs_for.h fitsExactCeiling). This template never assumes bytes — it only asks "does it fit". // Which rung is judged by which is the whole design: // (a) as built is judged by fitsExact — a document that already fits what its root PROMISES keeps everything, // and rung zero (the caller's droppable legend clauses, above this function in --for) is entered on the diff --git a/src/verbs_for.h b/src/verbs_for.h index 4ec8742f5..7ef3d69fa 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -1164,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 @@ -1188,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 @@ -1239,7 +1252,12 @@ inline std::string finishForLensHeader( std::string header, const ForLensRootFin // N1: onto the root — the same ">"; -inline ExpandServeChoice chooseExpandServe( std::size_t bundleBytes, const rw::WholeFileRender& wf, std::size_t budgetBytes ) +// ── 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; - // WHAT THE FILE CANDIDATE ACTUALLY COSTS (CodeRabbit, PR #215). This compared wf.rawBytes against a - // bundleBytes that is priced to the byte — root attrs, the unproven residue, the map, the bodies section — - // while the whole-file candidate's own legend is appended AFTER this decision and only in that mode. The - // file side was therefore under-priced by exactly this constant, so inside that band the tool chose, and - // REPORTED, the whole-file form as smaller while the bundle it rejected was the smaller document. The - // pack-budget arm above is deliberately left on rawBytes: that ceiling is about the FILE's own size - // against --pack-budget, not about which of two candidates is cheaper to serve. - const std::size_t wholeFileBytes = wf.rawBytes + kExpandWholeFileLegend.size(); - if( wf.complete && wf.rawBytes > budgetBytes ) - { - rw::formatTo( open, sizeof( open ), "", - wf.rawBytes, budgetBytes ); - } - else if( wf.complete && wholeFileBytes < 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 ), "", - wholeFileBytes, 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, wholeFileBytes ); + 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; } @@ -1829,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 ) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 929f6d0ab..83a72c6b7 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1840,13 +1840,29 @@ inline std::string forTaskText( const std::string& root, const std::string& task // 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 + "