diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 925aa09b..c0f8aa7c 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -2,18 +2,21 @@ # format: ack <16-hex-key> [cid=<16-hex-content-id>] [by=] — one per line, kept SORTED by (kind,key) on every write (merge-friendly) ack api-surface 048bde69cfc80a6f 2 cid=2dc85eb4d5e1cb33 lane V1 N2 (f5913f3): grepTierAttrs/grepTierKeys gain floorAlreadyEmitted, resolveCandidates gains capFired — one explicit parameter each, every caller updated in the same commit ack api-surface 060a064b6ffa7775 7 cid=605cbb1f768828e0 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign +ack api-surface 085e3d408c2c4a35 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 0a3d16d6f3139408 10 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 api-surface 0d6e1106ea5949d5 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 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 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 api-surface 105c48e20c80c896 3 cid=720fab31ea99ebde A2: unmeasuredHintNote gained the AbsHintFrame parameter one commit after this lane introduced it (4db6fb3). It is a header-inline helper in namespace mcpedit with exactly one caller, resolveOneForEdit, in the same file; no consumer outside this lane ever saw the 2-arg form. The widening is what makes the never-parsed disclosure and the symbol scan agree about which files a hint names -- two copies of that rule is the defect this replaces. ack api-surface 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept ack api-surface 10f47dd5a3f35d86 5 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 api-surface 131068a6cedf0864 3 cid=6df1f8794b53f5a2 E1 follow-up (#214 CI): prLegendText gains the one pre-render fact that gates testmap.h's run=/run_unknown=/ clause (corpusHasTests) — its single caller writePrContext passes it; the unconditional clause put defaultceilingcheck's 120-file no-test bundle 25 tokens over its 8000 default budget +ack api-surface 131068a6cedf0864 4 cid=9ead4ea91ba5d605 review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. | prior: E1 follow-up (#214 CI): prLegendText gains the one pre-render fact that gates testmap.h's run=/run_unknown=/ clause (corpusHasTests) — its single caller writePrContext passes it; the unconditional clause put defaultceilingcheck's 120-file no-test bundle 25 tokens over its 8000 default budget ack api-surface 1520fa02411735c3 6 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 api-surface 155d74d341a49f3c 2 cid=abcde655ff1c0826 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 15754e3561a34f40 7 cid=e3721579f68947f6 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 api-surface 163c0a0eb3219fa9 5 cid=9e7d5dab8c14a887 R2: prEmptyRootTail gains the truncated= parameter it needs to carry budget-floor-exceeded — deliberate, 1 caller, incompatible=0 (--edit-check contract-change); prEmptyRootPrice is the new file-scope helper that decides the label and re-prices, keeping writePrContext's own complexity and LOC unchanged | 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. ack api-surface 1689c98fa4eac33e 4 cid=f5ec9b69e2526e08 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 195e2b4deba2cee7 4 cid=c2581959226700a8 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. +ack api-surface 1a15386c2d1e47af 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 1c11c9480374c3a4 5 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: M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 1c873f03ef665f93 8 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 1da01868deceb731 7 cid=bce36ef4a75ab3fb 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) @@ -64,6 +67,7 @@ ack api-surface 5383f63ad718935c 7 cid=170e3fe466966e87 L10: printLintRuleTallyR ack api-surface 5391ffd9aa5765bf 6 cid=8a2aed10e80ed270 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack api-surface 53b3f823688054b4 5 cid=137a04e79db61a58 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). ack api-surface 55e8f57eb5817b60 3 cid=39ba1a3dc3bff198 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. +ack api-surface 56acdf9b5c314a14 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 5710beada2095a34 9 cid=3371683faa811117 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. | prior: the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack api-surface 574641dcc1bdf0ec 13 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 5774f0f445361430 5 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. @@ -76,6 +80,8 @@ ack api-surface 61e5df9e1e40ff70 5 cid=e2155dd6082b880a E2 (terminality round A, ack api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da 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 api-surface 654551bf984cc299 5 cid=913136f787574436 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 6e58b0a307757079 24 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 api-surface 6eef859578c8c376 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. +ack api-surface 6f2394762f82a855 2 cid=abcde655ff1c0826 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 75720b711509b5b9 5 cid=a54cd0ffa32a9952 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 api-surface 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. | prior: the --recall root-relative path-token round: the +1 param IS the root each scorer relativizes against, and the LOC is its rationale ack api-surface 77eb2194156050d0 4 cid=ae974822986c10b4 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. @@ -88,6 +94,7 @@ ack api-surface 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminali ack api-surface 7ed8ad2c213537a4 7 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack api-surface 7f2c3eefdf6e512e 3 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack api-surface 7f5e07a10dd97563 4 cid=1cc3fb32d2789e8a P2-2 regex hoist: passesPredicates takes the per-predicate compiled-regex table as one explicit parameter, every caller updated in the same commit (lane F; --lint/--match byte-identical) +ack api-surface 802e513731103806 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 80b08c75913ae76c 8 cid=2b1373e91d63396e 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 api-surface 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: 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 84b6bfc164c989e8 2 cid=87c39ba5f968fb34 M21(a) sa sym=/p=: staleAcksXml takes the caller's XML escaper as a template parameter (+1 param) because sym= carries a canonical id — corpus text — and quality.h sits BELOW serialize.h in the include order. testmap.h's runHint uses the same seam for the same reason; the alternative was including serialize.h from quality.h, which inverts the order. @@ -99,6 +106,7 @@ ack api-surface 8a92173ded649e17 14 cid=f76e97c84ac7c168 by=src/* lane 2 of the ack api-surface 8d58de9bb922f582 4 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 api-surface 925094be92085dae 3 cid=714cea1e1b31a1ae A6: rollbackMessage gained a 'cause' parameter one commit after this lane introduced it (57fe5fc). It is a header-inline helper in namespace rw::editplan with two callers, both in the same function in the same file; no consumer outside this lane ever saw the 2-arg form. The parameter is what lets the concurrent-write abort reuse the rollback disposition wording instead of growing a second copy of it. ack api-surface 92ac9caf38b8aab0 4 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 api-surface 93848617cae1fe4e 2 cid=a42863f577100fa6 A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface 95cc88ca4aab7039 4 cid=25c411d4871bda46 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 api-surface 96fdcdff2f0ff0f7 4 R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). ack api-surface 983814f2b5912a90 3 cid=fe2cfa34166f503a M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -114,6 +122,7 @@ ack api-surface aeed75863f7b617d 3 --lint reads the corpus ONCE (audit lane B2, ack api-surface b496a273ae1564ef 5 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 b65c78efe3e73a34 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 b689422adfc04435 5 cid=cdb3a6ea16c83186 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 api-surface b6a24afef32a68a8 2 cid=55e7936512b0563d A3: TestRunnerIndex takes the run's crawl root so run= is spelled relative to root= (one absolute root per document, test/rootrelemitcheck.sh ARM 9). The parameter is the fix; every caller passes the root it already holds. ack api-surface bb2c0b847815a0ca 4 cid=9eb5f97927595a07 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: the MCP edit_check verb mirrors the CLI pre-apply preview through the SAME editpreview::run, so the two surfaces cannot answer differently (gate arm N pins them document-for-document). new_body is optional and defaulted; every existing call site is untouched and the verb stays readOnlyHint true — passing it previews, it never writes. ack api-surface bcb3377087f2e034 7 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 be8176514288abc7 2 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. @@ -138,6 +147,7 @@ ack api-surface e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 ack api-surface e514a69013d0934c 6 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 ack api-surface e57abf94a3b1f3a4 5 cid=788aa47a3ec3d3f2 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 api-surface e6bdc5187566f3d7 8 cid=615b35e39fe420e4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) +ack api-surface ebaa8de80f3b5caa 2 cid=fbf73430469857ac A3 (review of #219), the other half of the TestRunnerIndex ack above: runHintClauseIfRows takes the root-relative predicate as well as the row count, because the clause's ROOT-RELATIVE sentence must answer to the same fact that spells run= itself — a multi-root run declares no root= and correctly keeps an absolute command, so a clause claiming relativity there is a wrong answer, not a cosmetic one. The parameter is the fix and there is deliberately no default: a defaulted false would let a caller silently drop the sentence, which is the silent-wrong-answer shape this lane exists to remove. All ten call sites pass the predicate they already hold (packtask, flipimpact, handoff, partition, prcontext x2, verbs_change x2, situ, testmap). Visible only now because #214 landed the one-parameter form onto main first; once this lane merges, main carries the two-parameter form and the row cannot recur. Proven by runhintcheck 2c/2d (the root-spelling matrix, and multi-root does not claim a relativity it does not have) and prcontextcheck 49 rows, full suite gates=629 pass=627 skip=2 fail=0. ack api-surface ed05ee0357d016f1 4 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. 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. @@ -600,6 +610,7 @@ ack duplication de772ce9497e6cc2 425 test/verify_strkern.cpp keeps VERBATIM copi ack duplication dec50094e5aef506 74 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 duplication df5a70dc16c85001 33 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication e0406bb747448d85 154 ingest() decomposition: emitBindingAliases/emitRouteDefs are the FFI-alias and route-def sort ladders moved verbatim out of ingest()'s body into named phase helpers (ingest_model.h); as inline statements the clone scanner could not see them, as small functions their comparator shape token-matches sortLintRows - different element types and orderings, no shareable contract, zero behavior change (argvdiff byte-identical) +ack duplication e2d22ac45d11d555 19 review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. ack duplication e3410f4eae133942 35 wave-3 close (--quality-delta=ec5e3c3..HEAD convergence): isShellBuiltinName takes externalnames.h's own house shape — a static_assert-sorted table read by binary_search/svLess, the one-liner its siblings isPythonBuiltin/isCFamilyStdName already are; the KindCounts::total match is token-shape only (accumulate over begin/end vs binary_search). The hand loop L7's P4 landed with cloned five unrelated predicates; this shape clones its two siblings, deliberately ack duplication e4a7b94d4f067230 125 ingest.cpp split 2026-08-29: pre-split clone pairs (buildNewlineOffsets vs the bench_newline_ab arms, acked lane-B3 at keys 98099b517e9d2fbb/a6f7de52f80c9f48) whose clone-group keys changed because buildNewlineOffsets moved VERBATIM into ingest_astquery.h — the disclosed clone-ack rename floor, same artifact as the main.cpp split's moved-clone row; argvdiffcheck vs c267a4b proves no body changed ack duplication e75742478839369d 31 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -685,6 +696,7 @@ ack params 19d43d944ddcd186 6 cid=533f2648b2fac227 timsort vendoring: every row ack params 19e15f944de795a8 6 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 1c11c9480374c3a4 6 cid=c705715192cb60ab lane for-widen (L-W): the file page, coverage= and the thin next= land in the three --for dispatch/emit bodies the 0.6.1 lanes touched this week — this lane's own churn, not thrash ack params 28c51bbd0032f9ec 9 cid=bdd33965f128de92 bench/capsweep write_screen renders the nine screen.tsv columns capsweepcheck pins by name; a record object would move the schema the gate reads +ack params 3478654139c90f0f 7 cid=8524565c31be122d review of #219: the run= relativity claim is CONDITIONAL, so the predicate has to reach every site that states it — prLegendText and writeFlipHeader take rootRelativeRuns (and its root= attribute) because the legend sentence and the command spelling must be decided by ONE fact, testmap.h runsAreRootRelative; a legend that claims a relativity the spelling does not have is the defect this round fixes. The duplication row pairs that 19-token predicate with mcp.h mcpOmitsGitVerbs, which asks a different question about a different field and shares only the shape of a two-term boolean return — merging them would name neither. ack params 3561d0281d324276 13 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b 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: 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 params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. diff --git a/CHANGELOG.md b/CHANGELOG.md index d9e64a82..20d8da11 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,237 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — a relative command with no anchor, and the roots that never declared themselves + +A second review of the three `--situ` entries below found twelve defects — counted one per +independently described correction below, which is four surfaces that spelled a path or a command +relative to a root they never declared, one in the shared path relativizer, three in the new +lexical-siblings block and four disclosure readings dropped when sentences became attributes (4 + 1 + 3 ++ 4) — every one of them a document that could not be resolved by the reader holding it, and all twelve +are fixed here. The two byte ratchets named at the end moved with the fixes and are not counted among +them: a pin is not a defect. A third review, of this entry's own fixes rather than of the entries below +it, found two more — the filesystem root in that same relativizer, and a quadratic scan in the new +lexical-siblings block — and both are fixed here too. A fourth review found a FIFTEENTH in shipped +output: a `run=` command did not shell-quote its path. Separately, this lane's own new cap left two +published counts stale (README's cap total and `docs/TUNING.md`); both were republished by their +generators rather than edited, and they are drift this entry caused rather than a sixteenth defect. + +A run= IS A COMMAND, AND ITS PATH COMES FROM THE CORPUS. `testmap.h`'s `spell()` concatenated the +runner verb and the path, so a repository containing the legal filename `test/check;touch PWNED.sh` +made the tool emit `run="bash test/check;touch PWNED.sh"` — a command this tool hands an agent to +paste, which runs `touch PWNED.sh` in the reader's shell (CWE-78, external reachability). The path is +now always one shell argument. It is quoted only when it is not provably safe, and that is measured +rather than preferred: `shSingleQuote` always wraps, so quoting unconditionally would move the `run=` +bytes of every test row in eight emitters — 13 literal command assertions across 7 gates, +`docs/COMMANDS.md`, 15 committed capture snapshots, README and the `printf_parity` pins. Every path +`git ls-files` tracks here is inside the safe allowlist (`[A-Za-z0-9._/-]`, never a leading `-`, which +a shell reads as a flag), measured at 0 outside it, so the conditional form is byte-identical on every +real corpus while a hostile name is still quoted — `printffmtparitycheck` needed no re-pin. The +predicate is an allowlist, so an unenumerated byte is quoted by default. `test/runhintcheck.sh` arm (5) +EXECUTES the emitted command in a scratch corpus and asserts the payload did not fire, with the +unquoted spelling as its control. Red before the fix: the emitted command created the sentinel file. + +AND QUOTING WAS NOT SUFFICIENT — the same trust boundary, one layer in. A root-level +`-cimport os;open("PWNED","w")#_test.py` passes `isTestPath`, keeps its leading dash, survives quoting +intact, and then `python3` reads `-c` as "execute this code": the path reaches the INTERPRETER as an +option rather than the shell as code. Measured, both directions, over the whole population of verbs +`runnerVerb` can emit (exactly two — `.sh`→`bash`, `.py`→`python3`): `python3 ''` exited 0 and +created the payload file, `python3 -- ''` exited the file's own 7 and did nothing, and +`bash -- ''` likewise. `bash` did not reproduce the bypass with the equivalent payload (it +rejected the combined `-c` form, rc=1), so the confirmed case is `python3`; `--` is emitted for both +because both honour it. An option terminator now precedes any path that is not provably safe — a +leading `-` is already outside the allowlist, so this too is byte-identical on every real corpus and +`printffmtparitycheck` still needs no re-pin. `runhintcheck` arm (6) pins it, with the +quoted-but-unterminated spelling as the control that proves quoting alone was not the fix. + +A RELATIVE COMMAND IS ONLY AS GOOD AS ITS ANCHOR. Making `run=` root-relative is what makes a change +report independent of where the tree is checked out — and it makes every one of those commands useless +to a reader who cannot tell what they are relative to. Four surfaces had exactly that hole. The shared +run-hint clause claimed "relative to root=" unconditionally, including on a MULTI-root run, which +declares no `root=` at all and (correctly) keeps the absolute command: the spelling and the sentence now +answer to one predicate, `testmap.h runsAreRootRelative`, so they cannot disagree. `--flags --flip` +spelled every `p=` relative to the crawl root and declared no root either; `` now carries `root=` +with the one sentence that defines it, like every other verb. The MCP edit receipt — the surface that +hands a caller a command to paste — spelled `file`, every `tests_to_run[].run` and its stderr `next:` +relative to a root it never named; it now carries `"root"`, single-root only, the same condition every +other `root=` keeps. And `--help` still told the reader `run=` was "spelled with the same root you +scanned", which stopped being true in this lane; `--help` and `docs/COMMANDS.md` now say what the code +does. + +A "./" THAT RETURNED TOO EARLY. `sarif.h rootRelativeUri` stripped a stored path's leading `./` and +RETURNED, before the root prefix was ever tried. That is right for the root `.` and wrong for every +other relative spelling: `ripwire ./corp` stores `./corp/test/x.sh`, the early return yielded +`corp/test/x.sh`, and pasting that from the root the document declares is `cd ./corp && bash +corp/test/x.sh` — rc 127. Both sides now drop the optional `./` first and compare what is left, which +leaves the one case the early return got right byte-identical. `test/rootrelemitcheck.sh` ARM 9b turns +the old spelling pair into a matrix: `.`, `corp`, `./corp`, `corp/`, an absolute path and a symlink all +print the SAME command, and each printed command is EXECUTED from the root it names. + +AND THE ONE ROOT THAT IS ITS OWN SEPARATOR. That same relativizer then matched a prefix only when the +byte after it was a `/`, which the filesystem root can never satisfy: under `ripwire /` the stored +spelling is `/test/check.sh`, the byte after the prefix is `t`, and the ABSOLUTE path was emitted into a +document whose `root="/"` declares every path relative to it — `testmap.h runsAreRootRelative` is true +for any single non-empty root, `/` included, so the envelope's claim and its own rows disagreed. This is +every `p=`/`uri=` emitter in the tool, not SARIF alone: all of them route through this one pair. The +added clause strips the single leading slash, runs AFTER the general shape so no other prefix changes by +a byte, and is guarded on length so a file spelled `/` stays `/` rather than becoming an empty URI. A +corpus at the filesystem root means crawling the whole machine, so no end-to-end arm can reach it; +`test/sarifcheck.sh` arm 11 is a unit driver over the function itself, compiled with the flags CMake +gave the binary under test (the recipe `extentcheck.sh` (U) and `jsonwalkcheck.sh` already use). Its 22 +rows pin both halves together — the predicate the envelope claims and the URI the relativizer returns — +and the non-root prefixes sit in the same table, so an over-stripping fix fails there rather than on a +consumer's machine. Red before the change: 4 of the 22 failed, including `rootRelativeUri( +"/test/check.sh", "/" )` returning `/test/check.sh`. + +A SMALL BLOCK PAGED WITH SOMEONE ELSE'S WINDOW. The new lexical-siblings block honoured `page.offset` — +which is section `[1]`'s blast-radius offset. `--situ=F --offset=20` printed `shown=0 total=9 capped=1` +with a `next:` offering `--limit=9`, relief that cannot restore rows an OFFSET removed, and `--offset=7` +dropped six rows silently. It is a small fixed block with a cap, like the decl/def partner rows above it: +cap and `--limit`, no offset. In the same family, `unindexed_rows_floor` was computed only for a NON-EMPTY +list and the emitter suppressed the empty one, so the case where the crawl's 500-row cut removed the only +candidate printed nothing at all — a silent zero, which is the one thing METHODOLOGY §9 forbids outright. +The floor is a property of the candidate list, not of the answer: it is recorded whenever that list was +short, and the block speaks at zero. The MCP twin's `siblings_total` was the length of the array beside it +— a tautology — and now states `siblings_capped` explicitly beside a population. + +A CAP THAT BOUNDED THE ANSWER AND NOT THE WORK. That same lexical-siblings block compared every +unchanged indexed file and every unsupported crawl row against every changed path, and +`isLexicalSiblingOf` re-split both paths into directory and stem on each pair; the sibling ROW cap +applies only after collection, so it bounded what was printed and never what was computed — +O( (F + U) x C ). SAME DIRECTORY is the rule's most selective clause, so the changed paths are now +indexed by directory once (a sorted vector and a `lower_bound`, not a hash or tree map) and a candidate +is compared only against the changed paths sharing its directory: one `dirOf` and one binary search per +candidate, nothing more for a candidate whose directory nothing changed in. The predicate is still +`isLexicalSiblingOf`, called on the narrowed range rather than restated, so the rule cannot drift from +the prose that documents it. METHOD: the two implementations are timed on the function itself — its only +inputs are `ing.files` and the changed-file bitmap — built `-O2` with the flags CMake gives the shipped +binary, the two binaries interleaved, best of 5, two passes. The path population is a real +`llvm-project` checkout (`4d5358b1`, clang+llvm, 8,856 paths) and `golang/go` (12,555 paths), plus that +llvm population grown to the 182,555-file rung of `docs/EVALS.md` by re-rooting whole copies of the tree +— synthetic in SIZE only, every path keeping a real directory shape and a real stem. The changed set is +spread evenly rather than clustered, because a clustered one makes every candidate's FIRST comparison hit +and understates the old cost. The host was at load average 38 on 18 cores (other work in flight), so the +absolute figures are upper bounds and the interleaved RATIO is the measurement: at 182,555 files C=500 +2.20 s → 9.7 ms (226x) and C=2,000 9.10 s → 22.6 ms (403x); at the real 8,856-file rung C=2,000 333 ms → +11.6 ms (29x); on `golang/go` C=2,000 449 ms → 42.8 ms (10x). The rows are unchanged and proven so: the +emitted row list is byte-identical between the two implementations on all nine rungs measured (1,413 rows +in total), and `test/situshapecheck.sh` arm (7) already pins the case this narrowing could break — a +same-stem DECOY in another directory stays excluded, beside the same-directory-different-stem row, the +test partner and the changed file itself. No timing gate is added; there are none here. + +AN ATTRIBUTE WITHOUT A READING IS A TOKEN, NOT A DISCLOSURE. The compression below shortened four +sentences into attributes, and four readings went with them: what makes the counts a floor (call edges are +name-based), what an unindexed file IS, which header the resolver gauges come from, and whose cap +`prcontext_cap=` is. `--situ` is the one dialect with no legend anywhere to look a name up in — it refuses +`--legend=compact` — so each gauge keeps a short gloss, and `test/situshapecheck.sh` arm (8) asserts the +READING, not the token. Two byte ratchets moved with them (floor 200 → 360, partner 140 → 220): a ratchet +that forbids a restored disclosure is a ratchet aimed at the wrong thing. + +Measured with `wc -c`, this lane's base binary (`6621370f`) against this one over the SAME tree, so the +pair carries all three `--situ` entries below together. On this repo (root 131 chars): +`--situ=src/graph.h` 4,448 → 2,955 B, `--situ=src/situ.h` 2,332 → 2,040 B, `--situ=src/testmap.h` +2,325 → 2,033 B, `--test-gate=src/testmap.h` 5,455 → 5,247 B. On RocksDB @0e2801ac (root 66 chars): +`--situ=db/write_batch.cc` 7,489 → 7,376 B, `--test-gate=db/write_batch.cc` 9,946 → 9,868 B, +`--affected=db/write_batch.cc` 7,124 → 7,113 B. Gates: `test/situshapecheck.sh` (17 rows red on that base +binary, arms (8)–(11) new), `test/rootrelemitcheck.sh` ARM 9b/9c/9d, `test/runhintcheck.sh` 2c/2d, +`test/receiptpostcheck.sh` (18). Two gate self-checks were wrong in the same way the code was — an empty +`run=` made `eval ""` succeed, and an empty `next=` fell out of an if/elif chain printing neither PASS nor +FAIL — so each now reds on the outcome it exists to forbid. `situStemOf` was a fourth spelling of +`stripExt( baseNameOf( p ) )`; one `mention.h pathStem` now serves all four call sites. Pins moved: +`test/testgatelegendbudgetcheck.sh` 3,000 → 3,070 B for the 56 B conditional root sentence (measured +2,957 → 3,013 B on its `src/model.h` fixture), and `test/printf_parity.manifest` for `pack_task` and +`help_all`, the two labels whose text this round changed. The cap inventory is 211, not the 210 the +reference-guide entry below records: this PR's own sibling-row cap is the 211th, and `README.md` and +`docs/TUNING.md` were regenerated to say so (`test/readmedriftcheck.sh` (L2), `test/capsweepcheck.sh` (C)). + +### Added — `--situ` lists a changed file's lexical siblings + +The files that move WITH a changed file are usually its neighbours by name, and the caller walk can reach +none of them: a header does not call the source that implements it, an `.inl` is not indexed by any grammar +in any build, and a harness the graph cannot link — a fixture-built test, a generated `main` — is reached by +nothing. A byte-and-answer attribution over a frozen 30-question set found two answers incomplete for +exactly that reason. Section `[1]` of `--situ` now lists them, under the decl/def partners and the floor +clause: `lexical siblings (N) not_dependents=1 — same directory and stem as a changed file (its header/impl +partner, its test, its .inl): NOT transitive dependents, so they are absent from the list below; lexical and +static, never a graph result`, then one root-relative path per row. The rule is the +dumbest one that is always right — same directory, and the same filename stem or the stem-partner convention +the tests-to-run rows already use (`_test`, `test_`, `Test`, `_unittest`, `_spec`). Same +directory is load-bearing rather than a speed trick: a same-stem file in another directory is a namesake, not +a partner, and listing namesakes would make the block noise on exactly the large trees it is for. The +candidate population is the CRAWL's, not the index's, so an `.inl`/`.ipp`/`.tcc` partner — the sibling a C++ +change most often has to edit, and one no grammar can read — is named; the crawl's unsupported-extension row +list is itself capped, and the one case where that can shorten this list is disclosed as +`unindexed_rows_floor=1` — on the EMPTY list too, because a cut that removes the only candidate is exactly +the case a silent zero would hide. The block is capped at 8 rows with `shown=`/`total=`/`capped=1` and a +pasteable `next:`, and `--limit=N` raises it like the report's other two listings; it does NOT take section +`[1]`'s `--offset`, which is the blast-radius window's, so no offset can empty it. The MCP +`situational_awareness` twin carries the same list as `siblings` with `siblings_total`, `siblings_capped` +(always emitted: that payload serves every row, and an absent flag would be the silence this rule forbids) +and `siblings_unindexed_rows_floor`. It costs what it lists: the block's own rendered lines on RocksDB +@0e2801ac at `--situ=db/write_batch.cc` are 276 B — a 244 B header and one 30 B row naming +`db/write_batch_test.cc`, which no other section of that report reaches; on this repo, where every source +file is a lone `.h`, no file has a lexical sibling and the block prints nothing at all. Gate: +`test/situshapecheck.sh` arms (7)–(7d) on a fixture with a `.h`/`.cc`/`_test.cc`/`.inl` quadruple, a +same-stem DECOY in another directory and a same-directory file with a different stem — both must be absent — +plus a nine-sibling stem for the cap and its disclosure, a no-git copy of the same tree proving the block is +static (and therefore cannot leak), and the MCP twin agreeing row for row. All four arms red on the previous +binary. + +### Changed — `--situ`'s disclosures are attributes + +`--situ` is the only report with no XML root to hang attributes on, so every disclosure it owed was written +as a sentence, and the sentences grew: the graph-count floor clause ran 601 B, the decl/def partner header +228 B, the tests-to-run header 233 B and the script-gate caveat 167 B — 1,229 B of prose on every call, +carrying facts a reader can only act on once they are named. They are now named, and the four lines together +are 905 B. The floor line is `counts_floor=1 graph_ambiguous=N graph_unresolved=N graph_unindexed=N (the map +header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic +dispatch, callbacks and macros can be missing; a zero is "none found", never "none exists"` (601 → 344 B), +using the same attribute spellings the XML and JSON dialects already use, so the three share one vocabulary. +The partner header carries `not_dependents=1` (228 → 209 B), section `[1]` carries `prcontext_cap=20` where +it used to spell `--pr-context`'s own cap as an aside, section `[2]` carries `order=evidence` — the attribute +`--affected`'s root already carries for the same ordering (233 → 220 B) — and the script-gate blind spot is +`script_gates_unmodelled=N`, the same counter `--affected` publishes, with its cause kept (167 → 132 B). An +attribute is shorter than a sentence; it is not shorter than the FACT, so every gauge keeps a short gloss — +this is the one dialect with no legend anywhere to look a name up in (`--situ` refuses `--legend=compact`). +Nothing was dropped: every floor, cap and caveat survives, and the readings that have no attribute form (how +to read a zero; what `[changed]`/`[partner]`/`hops` mean on a row) stay as the shortest sentence that defines +them. The four line lengths above are the gate's own `${#line}`, measured on this repo at +`--situ=src/graph.h` (the partner header on the gate's fixture at `--situ=core/widget.cc`, since this repo +has no decl/def partner for `graph.h`), against the binary this lane branched from (`6621370f`) over the same +tree — one number, one corpus, and the gate's header carries the same table. The whole-report numbers for +this lane are in the review entry above, where they belong: the same pair of binaries also carries the +relativized `run=` and the new sibling block, so no single entry owns them. The gate is the new +`test/situshapecheck.sh`: one arm per converted disclosure, each +asserting the attribute is present, that its value agrees with the XML sibling's where one exists +(`graph_unindexed=`, `script_gates_unmodelled=`), that the reading survives, and a per-line byte ratchet so +the prose cannot creep back; 10 of its rows are red on the previous binary. `test/floormarkcheck.sh` keeps +the two anchor phrases it matches — `counts_floor=1` and "is a FLOOR, never a total" — and situshapecheck +mirrors them, so a regression reds in both. + +### Changed — one absolute root per change report + +`--test-gate`, `--situ` and `--affected` state the crawl root once, in the envelope (`root=` in XML and +JSON, the `root:` line in `--situ`'s text), and every path below it is relative to that root — which is +what makes the document independent of where the tree is checked out. One emitter never joined: the +`run=` command. It pasted the stored disk path verbatim, so on an absolute root `--test-gate` printed +the checkout prefix three times (the anchor, `next=`, and every `` row's `run=`) and `--situ` once per +runnable test line: a per-row cost against a per-document fact. The runner index now takes the run's root +and spells the command through the same relativizer every `p=` beside it uses, at all fourteen sites that +build one, so the twelve emitters sharing it cannot disagree; a multi-root run, whose disk path lies under +no single root, keeps the absolute command rather than become relative to a root that does not contain it. +The rule is stated where it is consumed: the shared run-hint clause gains "A run= command is relative to +root=: run it from there." (56 B, emitted only on a document that has rows AND a single root — a multi-root +run declares no `root=` and keeps the absolute command, so the sentence would be a false claim there) and +`--situ`'s `[2]` header says "a (run: …) is relative to root:". The saving is one root spelling per echo +less that clause, so it grows with checkout depth and with how many rows have a runner at all; the +whole-report numbers for this lane are in the review entry above, measured against the binary it branched +from over one tree. The gate is a new +ARM 9 in `test/rootrelemitcheck.sh`: a fixture carrying a real runner script, at two checkout depths, over +the eight verbs that echo a command — one anchor per document, no absolute path anywhere else, +byte-identical documents at both depths, and the printed `run=` actually executed from the declared root. +Red first on the unchanged binary (8 FAIL rows); `test/runhintcheck.sh`'s pins move with the contract, and +`test/printf_parity.manifest` moves for `--pack-task` alone, the one verb whose legend text changed. + ### Added — `--in=DIR` scopes the recent-changes block to a directory and stubs the map it was not asked for "What changed recently in DIR?" is six of the thirty questions in this project's frozen reference set (the diff --git a/README.md b/README.md index 229181a9..6206fbfc 100644 --- a/README.md +++ b/README.md @@ -1805,9 +1805,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-616 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +617 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 **616 gate scripts** and is the authoritative list; +`test/regression.sh` names **617 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 @@ -2543,7 +2543,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 211 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 212 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 978647dd..72a7d22d 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1144,7 +1144,7 @@ $ ./build/ripwire . --handoff **Answers:** before a PR: name the tests to run and the untested blast radius; -exit 4 if either is non-empty agent self-check before a PR (pair with --quality-delta): names the tests to run + the UNTESTED blast radius; exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff) run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries run="" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem matches the harness's, or whose TEXT names the harness file. Spelled with the same root you scanned, so it pastes straight into a shell. NO run= means NOT DERIVABLE -- never a guessed suite command +exit 4 if either is non-empty agent self-check before a PR (pair with --quality-delta): names the tests to run + the UNTESTED blast radius; exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff) run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries run="" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem matches the harness's, or whose TEXT names the harness file. Spelled RELATIVE to the root= the document declares, so it pastes into a shell run from there, and the document does not change with where the tree is checked out (a MULTI-ROOT run declares no single root, so it stays absolute). NO run= means NOT DERIVABLE -- never a guessed suite command **Try it** diff --git a/docs/EVALS.md b/docs/EVALS.md index b2eb8f4d..18e52d8c 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` | 616 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 617 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 **616 gate scripts**, all of which exist on disk. +naming **617 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 616. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 617. 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/LIMITS.md b/docs/LIMITS.md index 15cff728..08c83fce 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -16,10 +16,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 211 | 84 | 117 | **94** | +| 212 | 84 | 118 | **94** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 211 + 7 is the 218 constants this generator parses out of `src/`. +are not counted as caps, and 212 + 7 is the 219 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -37,8 +37,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **114 of 211 caps are classified -(39 INDEXING, 40 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **114 of 212 caps are classified +(39 INDEXING, 40 OUTPUT, 35 BOUNDARY); the remaining 98 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -88,7 +88,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 84 files that declare a cap — the 211 caps counted above, and no parameter. +One table for each of the 84 files that declare a cap — the 212 caps counted above, and no parameter. ### `src/abicheck.h` @@ -559,7 +559,7 @@ Discloses: **none** ### `src/mcpverbs.h` -Discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `unindexed_candidates_capped` +Discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `siblings_capped`, `unindexed_candidates_capped` | constant | value | class | note | | --- | --- | --- | --- | @@ -800,6 +800,7 @@ Discloses: `tests_capped`, `untested_capped` | `kSituBlastFilesShown` | `8` | OUTPUT | section [1] — blast-radius file rows; a raisable DEFAULT | | `kSituPartnerFileRowsShown` | `4` | OUTPUT | section [1] — decl/def partner rows | | `kSituPartnerRowsShown` | `8` | OUTPUT | section [3] — co-change partner rows; a raisable DEFAULT | +| `kSituSiblingRowsShown` | `8` | — | section [1] — L-D lexical sibling rows; a raisable DEFAULT | ### `src/skillscan.h` diff --git a/docs/TUNING.md b/docs/TUNING.md index 4bcaf7e8..de2807fe 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 129 | 128 | 112 | 12 | **37** | 75 | +| 130 | 129 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **129 cap -declarations** under **128 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **130 cap +declarations** under **129 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 128 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **5 declared since the sweep was prepared, +every run, which makes them a third population again. Enumerated over the 129 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **6 declared since the sweep was prepared, which no measurement has touched** (`kChurnMergeBombMaxFiles`, `kFieldIdCapacity`, -`kForPageRowsDefault`, `kForPageUnionSymbolCap`, `kMaxBlockBytes`). 111 + 12 + 5 = 128, and `emit` -refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than -111 because one name the sweep classified is no longer declared in `src/` at all: -`kSituTestRowsShown`. Quoting "113 of 129" would be wrong in both halves at once, which is the shape -of error a generated table exists to prevent. +`kForPageRowsDefault`, `kForPageUnionSymbolCap`, `kMaxBlockBytes`, `kSituSiblingRowsShown`). 111 + +12 + 6 = 129, and `emit` refuses to render a partition that does not add up. The `tunable` column +above reads 112 rather than 111 because one name the sweep classified is no longer declared in +`src/` at all: `kSituTestRowsShown`. Quoting "113 of 130" would be wrong in both halves at once, +which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 129-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 130-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. @@ -308,7 +308,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kBatchCap` = `16` -`src/mcpverbs.h` — discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `unindexed_candidates_capped` — probe value `128` — **1 verb(s) respond** +`src/mcpverbs.h` — discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `siblings_capped`, `unindexed_candidates_capped` — probe value `128` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index a0c7c6ea..2170a6b3 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 = [ - ["616 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 + ["617 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, "616", "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, "617", "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 }){ ["180 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"], - ["616 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["617 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 2d9c6958..6cc86354 100644 --- a/src/cli.h +++ b/src/cli.h @@ -1175,8 +1175,10 @@ inline constexpr char kHelpHead[] = " exit 4 if either obligation is non-empty (run the tests, then rely on green). (default = git diff)\n" " run= on a test row --affected/--situ/--test-gate/--exercises/--pr-context/--pack-task name harness FILES, not commands. A row carries\n" " run=\"\" when a runner is DERIVABLE from real evidence: a test-dir .sh/.py whose basename stem\n" - " matches the harness's, or whose TEXT names the harness file. Spelled with the same root you scanned,\n" - " so it pastes straight into a shell. NO run= means NOT DERIVABLE -- never a guessed suite command\n" + " matches the harness's, or whose TEXT names the harness file. Spelled RELATIVE to the root= the\n" + " document declares, so it pastes into a shell run from there, and the document does not change\n" + " with where the tree is checked out (a MULTI-ROOT run declares no single root, so it stays\n" + " absolute). NO run= means NOT DERIVABLE -- never a guessed suite command\n" " --grep=STR | --regex=PAT search for a literal or a regex; every hit comes back with its enclosing symbol\n" " literal / regex search + enclosing symbol + the matched line. SPAN-TIERED by default (see\n" " --grep-in below): the scan itself is exhaustive, the ANSWER serves one tier and discloses\n" diff --git a/src/flipimpact.h b/src/flipimpact.h index f8965485..76d9baa8 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -1143,8 +1143,11 @@ inline constexpr const char* kFlipRowLegend = // `testFilesRendered` is the count testmap.h's seam returns for the listing this header introduces — // review of #214: the run-hint clause was spliced unconditionally, so a flip with paid 180 B // for a rule about rows it has none of. The caller renders the rows first and passes the count it got. +// `rootRelativeRuns` is testmap.h's runsAreRootRelative for this run, decided by writeFlip (which holds the +// ingest and the root) and passed in: the legend sentence and the command spelling answer to one predicate. inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex, - const std::string& nextInvocation, std::size_t testFilesRendered ) + const std::string& nextInvocation, std::size_t testFilesRendered, + bool rootRelativeRuns, std::string_view rootAttr ) { rw::emitTo( out, "", // M21(b): the run=/run_unknown= rule, from testmap.h's ONE constant — rows-gated, through // the ONE gate every other legend asks (runHintClauseIfRows). - std::string( rw::runHintClauseIfRows( testFilesRendered ) ).c_str(), kFlipRowLegend ); + rw::runHintClauseIfRows( testFilesRendered, rootRelativeRuns ).c_str(), kFlipRowLegend ); + // Review of #219: every p= this verb prints is already spelled relative to the crawl root (relForHash), + // and declared no root at all — so a consumer holding the document could resolve none of them, + // and the run= commands beside them had nothing to be relative to either. The attribute and the one + // sentence that defines it are emitted together, the same pairing every other verb's root= keeps. + rw::emitRaw( out, rw::rootRelPathsLegend( !rootAttr.empty() ) ); rw::emitTo( out, "", + " hosts=\"{}\" filescope=\"{}\" downstream=\"{}\" dependents=\"{}\" tests=\"{}\" untested=\"{}\" files=\"{}\"{}{}>", ex( res.name ).c_str(), darkflags::gateKindTag( res.kind ), ex( res.def ).c_str(), res.isDark ? 1 : 0, res.isRuntime ? 1 : 0, ex( res.defSite.path ).c_str(), res.defSite.line, res.family.size(), res.totalRegions, res.totalLines, res.branches.size(), res.bindings.size(), res.hosts.size(), res.fileScopeLights, res.downstream.size(), res.dependents, res.tests.size(), res.untested.size(), res.filesScanned, - rw::nextAttrXml( nextInvocation ).c_str() ); + rw::nextAttrXml( nextInvocation ).c_str(), std::string( rootAttr ).c_str() ); // the contradiction row: this gate is ALREADY lit by the winning declaration, and dark only in the other if( !res.isDark ) @@ -1239,11 +1247,15 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult // E1 / review of #214: the listing is rendered HERE, before the header, so the header's run-hint clause // can be gated on the rows this document will actually carry. TestRunnerIndex stays lazy — it reads a runner // script only when asked about a file, and an empty res.tests asks about none. - const rw::TestRunnerIndex flipRunners( ing ); + const rw::TestRunnerIndex flipRunners( ing, root ); const rw::JoinedTestRows flipTests = rw::testRowsList( flipRunners, rw::testRowsOutOf( res.tests, rel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, ex ); - writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ), flipTests.files ); + // The root every p= above is relative to. Single-root only, exactly like every other verb's root= + // (ing.realPaths is non-empty only on a multi-root merge, where there is no single root to name). + const std::string flipRootAttr = rw::runsAreRootRelative( ing, root ) ? ( " root=\"" + ex( root ) + "\"" ) : std::string(); + writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ), flipTests.files, + rw::runsAreRootRelative( ing, root ), flipRootAttr ); writeFlipLights( out, res, ing, ex, maxRows, pageOffset ); for( const ValueBinding& b : res.bindings ) diff --git a/src/graphlegend.h b/src/graphlegend.h index fa2464bc..5bf1a8ca 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -168,12 +168,24 @@ inline std::string graphCountFloorBrief( bool hasUnindexed ) return std::string( kGraphCountFloorBriefLegend ) + graphUnindexedLegend( hasUnindexed ); } -// The same two facts as PROSE, for the one graph-count report that is text (--situ's [1] blast radius). -// The trailing %s is the #66 gauge's prose clause — EMPTY when nothing went unindexed, so this dialect -// keeps the same omit-at-zero reading as the XML/JSON attribute rather than printing a bare "0" the other -// two dialects never print. Rendered through graphUnindexedTextClause() below, never spelled at the site. +// The same facts for the one graph-count report that is TEXT (--situ's [1] blast radius) — as ATTRIBUTES, +// not as a paragraph. A5 (2026-09-13, PLAN_OUTPUT_ROUTING_LOOP §1.5): this line ran 601 B on every --situ +// answer to say four things that are names with values — counts_floor, the two resolver gauges and, when a +// file could not be read at all, the third — plus the two readings a consumer cannot supply for itself: +// that the counts are floors, and how to read a zero. The names are now spelled exactly as the XML/JSON +// dialects spell them (graph_ambiguous=/graph_unresolved=/graph_unindexed=, counts_floor=1), so the three +// dialects share one vocabulary, and the two readings stay as the short clause after them. Nothing was +// dropped: METHODOLOGY §9 puts honesty in the attributes, and the sentence was never the honest part. +// The trailing {} is the #66 gauge — EMPTY when nothing went unindexed, so this dialect keeps the same +// omit-at-zero reading as the attribute rather than printing a bare "0" the other two never print. +// Review of #219: an attribute with no reading is a token, not a disclosure — and this is the ONE dialect +// with no legend to look a token up in (--situ refuses --legend=compact; compactlegendcheck (R)). So the +// floor's CAUSE (a name-based call graph) and what an unindexed file IS stay on the line. They are the +// price of having no legend, not prose the compression was entitled to. +// Gate: test/situshapecheck.sh (1) and (8); test/floormarkcheck.sh (9) keeps the two anchor phrases. inline constexpr const char* kGraphCountFloorTextLine = - " 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={} graph_unresolved={} is the whole graph's resolver gauge (calls split over several defs / calls whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved={}\n"; // std::format FORMAT: two gauge totals + the clause + " counts_floor=1 graph_ambiguous={} graph_unresolved={}{} (the map header's own gauges) — every count above is a FLOOR, never a total: " + "call edges are name-based, so dynamic dispatch, callbacks and macros can be missing; a zero is \"none found\", never \"none exists\"\n"; // std::format FORMAT: two gauge totals + the clause // The #66 clause for the prose dialect. "" at zero — the absence IS the confident case, same as the attribute. inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) @@ -182,10 +194,8 @@ inline std::string graphUnindexedTextClause( std::size_t unindexedFiles ) { return {}; } - char buf[256]; // literal ~180 B + one %zu at 20 digits = ~198 B worst case; snprintf truncates regardless - rw::formatTo( buf, sizeof( buf ), - "; graph_unindexed={} is a third gauge — files no grammar in this build could read at all (the map header's unindexed=), whose calls produce no reference and so raise neither gauge above", - unindexedFiles ); + char buf[128]; // literal 63 B + one size_t at 20 digits = 83 B worst case; snprintf truncates regardless + rw::formatTo( buf, sizeof( buf ), " graph_unindexed={} (files no grammar in this build could read at all)", unindexedFiles ); return buf; } diff --git a/src/handoff.h b/src/handoff.h index 77e9dc47..c191d40f 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -277,7 +277,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In // SAME facts — carried run="bash test/…" for those same files. The packet whose whole purpose is to be // read by the NEXT session was the one that said least. Built here, inside the section's scope, because // TestRunnerIndex is lazy: a packet with no test row reads no runner script. - const rw::TestRunnerIndex hoRunners( ing ); + const rw::TestRunnerIndex hoRunners( ing, root ); const auto hoEsc = [ & ]( std::string_view t ) { return std::string( escapeXml( t, esc ) ); }; // E1: where no runner is derivable. The seam returns the FILE count with the rows, and the legend // below is gated on it — review of #214: this packet spliced the clause unconditionally, and it is @@ -386,7 +386,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In const auto assemble = [ & ]( std::size_t keepRows, std::size_t withheld ) { std::string doc = kHandoffLegendHead; - doc += rw::runHintClauseIfRows( hoTests.files ); // M21(b): the ONE wording through the ONE gate — never a seventh paraphrase + doc += rw::runHintClauseIfRows( hoTests.files, rw::runsAreRootRelative( ing, root ) ); // M21(b): the ONE wording through the ONE gate — never a seventh paraphrase if( anySymsCapped ) { doc += handoffSymsCapClause(); } // absent unless an row was cut doc += kHandoffLegendTail; doc += " gives, through the SAME function — see // testmap.h::affectedAnswerForFile for why this used to be a private walk and what that cost. const AffectedAnswer ans = rw::affectedAnswerForFile( ing, g, fileId ); - const TestRunnerIndex runners( ing ); + const TestRunnerIndex runners( ing, root ); const auto jesc = []( std::string_view t ) { return mcpdetail::jsonEscape( std::string( t ) ); }; const std::string prefix = rw::sarif::rootPrefixOf( root ); std::string out = ",\"tests_to_run\":["; @@ -1129,7 +1129,17 @@ namespace mcpedit if( nextOut != nullptr ) { *nextOut = nextFlag( "--test-gate=", fileIdentity ); } return ",\"post_check_unavailable\":\"the edited file is not in the refreshed index\""; } + // Review of #219 (A3): every path this receipt hands back — "file", each tests_to_run[].run recipe and + // the stderr "next:" — is spelled RELATIVE to the crawl root, and the receipt named no root at all. + // An MCP client runs in its own working directory, so a relative command it cannot anchor is a + // command it cannot paste. The receipt's JSON siblings (--test-gate --json, situational_awareness) + // have carried "root" all along; this is the surface that least afforded to omit it. Single-root + // only, the same condition every other root= keeps. Gate: test/receiptpostcheck.sh (18). std::string out; + if( ing.realPaths.empty() && !root.empty() ) + { + out += ",\"root\":\"" + mcpdetail::jsonEscape( root ) + "\""; + } if( focus == kNoNode ) { // Honest, and it happens: a replace whose payload defines a DIFFERENT name leaves no definition @@ -1150,7 +1160,7 @@ namespace mcpedit // evidence order now, so next= suggests the changed/partner test ahead of a deeper graph hop const std::uint32_t firstTest = withTests ? rw::firstTestFileForFile( ing, g, editedFile ) : rw::kNoFile; *nextOut = receiptNextFor( fileIdentity, symbolName, out, - firstTest == rw::kNoFile ? std::string() : TestRunnerIndex( ing ).commandFor( firstTest ) ); + firstTest == rw::kNoFile ? std::string() : TestRunnerIndex( ing, root ).commandFor( firstTest ) ); } return out; } diff --git a/src/mcpverbs.h b/src/mcpverbs.h index f62936ba..073c3a87 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1227,9 +1227,14 @@ inline std::string situationDiffJson( const std::string& root, const std::string // CLI text twin (situ.h::writeSituation) states the SAME fact on its own leading "root: …" line. const bool situJSingleRoot = ing.realPaths.empty(); const std::string situJRootPrefix = situJSingleRoot ? sarif::rootPrefixOf( root ) : std::string(); + // L-D: the STORED-path form is the primitive (an unindexed sibling has no fileId); the fileId form is it. + const auto situJPathRelStr = [ & ]( std::string_view p ) -> std::string_view + { + return situJSingleRoot ? sarif::rootRelativeUri( p, situJRootPrefix ) : p; + }; const auto situJPathRel = [ & ]( std::uint32_t f ) -> std::string_view { - return situJSingleRoot ? sarif::rootRelativeUri( ing.files[f], situJRootPrefix ) : std::string_view( ing.files[f] ); + return situJPathRelStr( ing.files[f] ); }; const auto fileObj = [ & ]( std::uint32_t f ) -> std::string @@ -1238,7 +1243,7 @@ inline std::string situationDiffJson( const std::string& root, const std::string // §B6 M11: the run= hint index, from the SAME source --affected/--situ/--test-gate/--pr-context read // (testmap.h). runFieldJson is that header's JSON call shape, so "absent means NOT DERIVABLE" — the // load-bearing half of the rule — is decided in one place for every emitter rather than re-decided here. - const TestRunnerIndex runners( ing ); + const TestRunnerIndex runners( ing, root ); const auto jsonEsc = []( std::string_view sv ) { return mcpdetail::jsonEscape( std::string( sv ) ); }; // M10: this verb reads git (the diff itself, plus an 18-month co-change mine below) and, before this @@ -1329,7 +1334,36 @@ inline std::string situationDiffJson( const std::string& root, const std::string // F2: and the window the co-change zero below was mined in — a JSON reader that only sees an empty // `forgotten` array cannot tell "no partners" from "the window mined nothing", and this surface is the // one where that reads most like an answer. - out += "]" + declDefAndWindowJson( facts, situJPathRel ) + ",\"forgotten\":["; + // L-D: the same lexical siblings the CLI report's [1] section lists — the ONE list here whose members + // may not be indexed at all (an unindexed .inl has no fileId), so they carry the STORED spelling through + // the string relativizer rather than the fileId one. Served whole, like every other array in this payload. + std::string situJSibs = ",\"siblings\":["; + { + bool first = true; + for( const std::string& p : facts.siblings.paths ) + { + if( !first ) + { + situJSibs += ","; + } + first = false; + situJSibs += "{\"file\":\"" + mcpdetail::jsonEscape( std::string( situJPathRelStr( p ) ) ) + "\"}"; + } + } + // Review of #219: siblings_total used to be the length of the array beside it — a tautology a reader + // cannot act on. This payload's standing rule is that an absent limit serves EVERY row (see the header + // above), so the honest form is not a cap but the PAIR pageview.h rule 1 asks for: the population, and + // an explicit statement that nothing was cut. `false` is emitted, never omitted — an absent + // siblings_capped would be exactly the silence this fixes. + situJSibs += "],\"siblings_total\":" + std::to_string( facts.siblings.paths.size() ) + + ",\"siblings_capped\":false"; + if( facts.siblings.unindexedRowsFloor ) + { + // …and the population itself is a FLOOR when the crawl's unsupported-extension ROW list was cut: + // a sibling no grammar can read may simply never have been rowed. Rides the EMPTY list too. + situJSibs += ",\"siblings_unindexed_rows_floor\":true"; + } + out += "]" + declDefAndWindowJson( facts, situJPathRel ) + situJSibs + ",\"forgotten\":["; { bool first = true; for( std::size_t i = situJForgot.begin; i < situJForgot.end; ++i ) diff --git a/src/mention.h b/src/mention.h index 002713ef..21e3eb03 100644 --- a/src/mention.h +++ b/src/mention.h @@ -272,6 +272,11 @@ inline std::string_view stripExt( std::string_view name ) noexcept return ( dot == std::string_view::npos || dot == 0 ) ? name : name.substr( 0, dot ); } +// A path's STEM — its basename with the last extension dropped ("src/a/b_test.py" → "b_test"). Both halves +// already lived here; the PAIR was being re-spelled at four call sites (testmap.h twice, situ.h, binstale.h), +// which is exactly the new-clone-of-a-reused-helper --quality-delta reports. One name, one composition. +inline std::string_view pathStem( std::string_view path ) noexcept { return stripExt( baseNameOf( path ) ); } + // does `path` end with the mention's segments as whole path components (extension-agnostic on the last)? // e.g. segments [transformers, optimization] matches "src/transformers/optimization.py". inline bool pathSuffixMatches( std::string_view path, const std::vector& segments ) noexcept diff --git a/src/packtask.h b/src/packtask.h index 08aca82f..8016168e 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -1504,7 +1504,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, } // E1 / review of #214: the section cuts over its own GROUPED, ESCAPED rendering — see packTaskTestsSection // above for the defect that forced it and for why a bisection answers it exactly. - const rw::TestRunnerIndex runners( ing ); + const rw::TestRunnerIndex runners( ing, in.rootArg ); // A3: root-relative run=, same root the p= above are relative to PackTaskSection tests = packTaskTestsSection( runners, ptRows, testsBudget, kPackTaskWrapReserve, ex, &testPartition ); const std::size_t testsTotal = ptRows.size(); const MonotoneRoll testsRoll = monotoneRoll( tests.kept < testsTotal, testsBudget, tests.xml.size() ); @@ -1734,7 +1734,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, { char b[ 96 ]; rw::formatTo( b, sizeof( b ), ",\"tests_total\":{},\"tests_kept\":{},\"tests_to_run\":[", testsTotal, testsKept ); j += b; } // §A9.5: the JSON sibling of the XML run= above — situ's tests_to_run already carries it, and one // computation path must not serialize two different obligations. - const rw::TestRunnerIndex jsonRunners( ing ); + const rw::TestRunnerIndex jsonRunners( ing, in.rootArg ); const auto jrun = [ & ]( std::string_view s ) { return jsonStr( s ); }; const std::vector jKeptRows = rw::testRowsOutOf( std::span( testFiles ).first( std::min( testsKept, testFiles.size() ) ), jPathRel ); bool jFirstTest = true; @@ -1808,9 +1808,15 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // mcpattrparitycheck still sees one spelling on every root. droppedPositiveAttr += lr.capAttrs; + // The clause is now BUILT (the root-relative sentence is conditional, so the seam composes a string + // rather than handing back one of two constants), and PackTaskHeaderParts holds VIEWS — so it is owned + // by a named local here, like report and droppedPositiveAttr above it. Binding the view straight to the + // returned temporary is a dangling read the moment the full expression ends, and it showed as exactly + // that: packtaskcheck's bundle was both malformed and non-deterministic (two runs, two sha256s). + const std::string runClauseStr = rw::runHintClauseIfRows( tests.kept, rw::runsAreRootRelative( ing, in.rootArg ) ); // the ONE gate: the section's own kept count const PackTaskHeaderParts headerParts{ task, rootOpenStr, taskNote, mentionNote, boostNote, docMentionNote, sibliftNote, expandNote, report, droppedPositiveAttr, in.rootArg, - rw::runHintClauseIfRows( tests.kept ) }; // the ONE gate: the section's own kept count + runClauseStr }; const auto buildHeader = [ & ]( bool withRouteAttr, bool withTaskEcho, std::string_view extraNotes ) { if( in.innerBundle ) // P10 (L7): a partition slice — the outer legend speaks once for all of them diff --git a/src/partition.h b/src/partition.h index b074714c..8912ac08 100644 --- a/src/partition.h +++ b/src/partition.h @@ -609,7 +609,7 @@ inline std::string packTaskPartitionText( const IngestResult& ing, const Graph& sliceTests += part.testsKept; } whole += ""; whole += bundleOpen( "core", -1, core ); whole += core.xml; diff --git a/src/prcontext.h b/src/prcontext.h index 285e5935..b99e6b21 100644 --- a/src/prcontext.h +++ b/src/prcontext.h @@ -662,10 +662,18 @@ inline constexpr std::string_view kPrEstUnmeasuredLegendClause = // decided by different facts — the chosen body's test-row COUNT, and whether that body could be measured at // all. Both are gated for the same measured reason (see kPrEstUnmeasuredLegendClause): a clause that states a // rule about something this document does not contain is bytes every reader pays for and no reader needs. +// MERGE of #214 and #219, and the third field is why this is a union and not a choice. #214 replaced two +// bare bools with this struct; #219 (A3) had made the run clause's ROOT-RELATIVE sentence conditional, so the +// clause is no longer the constant kRunHintLegendClause but whatever testmap.h's runHintClauseIfRows returns +// for this run. Taking either side whole drops the other's fact: main's spelling loses the root sentence, +// ours loses the est-unmeasured clause. `rootRelativeRuns` is not a third GATE — it selects which run clause +// is spliced once runHint has already decided that one is — and it answers to the SAME predicate that spells +// run= itself (testmap.h runsAreRootRelative), so the sentence and the spelling still cannot disagree. struct PrLegendClauses { - bool runHint = false; // M21(b)/E1: testmap.h's run=/run_unknown=/ rule — rides a rows-bearing body - bool estUnmeasured = false; // review of #214: the est-unmeasured label's definition — rides a document carrying the label + bool runHint = false; // M21(b)/E1: testmap.h's run=/run_unknown=/ rule — rides a rows-bearing body + bool estUnmeasured = false; // review of #214: the est-unmeasured label's definition — rides a document carrying the label + bool rootRelativeRuns = false; // A3 / review of #219: the run clause's root-relative SENTENCE — one declared root, or the command stays absolute }; inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindexed, const PrLegendClauses& clauses ) @@ -696,7 +704,9 @@ inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindex // --impact reports, so the same floor applies to hundreds of attributes in this one document. // The shared constants, never a pr-context wording — that is the §B4 echo-site rule. + rw::graphCountDisclosure( hasUnindexed ) - + std::string( clauses.runHint ? rw::kRunHintLegendClause : std::string_view() ) // M21(b)/E1: the row's run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated + // Rows-gated through testmap.h's OWN seam rather than its bare constant: runHintClauseIfRows is + // what appends the root-relative sentence, so #219's A3 fix survives #214's struct. + + rw::runHintClauseIfRows( clauses.runHint ? 1 : 0, clauses.rootRelativeRuns ) + std::string( clauses.estUnmeasured ? kPrEstUnmeasuredLegendClause : std::string_view() ) // review of #214: the est-unmeasured label's definition — label-gated, same reason + "-->"; } @@ -992,12 +1002,16 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // E1: both legend forms are built now and ONE is written later, once the body is known — writeHead takes // that body's own PrTrimRender::testFiles count. The envelope is priced without the clause and the pricer - // adds runClauseBytes for a rows-bearing body. - const std::string legendText = prLegendText( escBase, g.unindexedFiles > 0, PrLegendClauses{} ); + // adds runClauseBytes for a rows-bearing body. A3 / review of #219: the run clause's ROOT-RELATIVE + // sentence is conditional too, so every form is built with the one predicate that also decides the run= + // spelling — carried in the clause struct rather than as a second bare bool. + const bool prRootRelRuns = rw::runsAreRootRelative( ing, root ); + const std::string legendText = prLegendText( escBase, g.unindexedFiles > 0, PrLegendClauses{ .rootRelativeRuns = prRootRelRuns } ); const std::string anchorNoteText = prAnchorNoteText( anchorAttr ); // The clause-bearing form is built ONCE, and only if it is the form that gets written — the difference - // between the two is exactly kRunHintLegendClause (prLegendText splices that constant and nothing else), - // so the pricer reads the constant's size rather than a second rendering's. + // between the two is exactly what testmap.h's runHintClauseIfRows returns for this run (prLegendText + // splices that and the est-unmeasured clause, nothing else), so the pricer below asks that same seam for + // its size rather than measuring a second rendering. // Review of #214: `unmeasured` is the SECOND rows-style gate — the est-unmeasured clause rides only the // document whose chosen level could not be measured, and the pricer charged it on exactly that fact // (PrTrimRender::rendered), so the written legend and the priced legend are the same bytes. @@ -1005,7 +1019,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest { const std::string legend = ( testFiles > 0 || unmeasured ) ? prLegendText( escBase, g.unindexedFiles > 0, - PrLegendClauses{ .runHint = testFiles > 0, .estUnmeasured = unmeasured } ) + PrLegendClauses{ .runHint = testFiles > 0, .estUnmeasured = unmeasured, .rootRelativeRuns = prRootRelRuns } ) : legendText; std::fwrite( legend.data(), 1, legend.size(), out ); std::fwrite( anchorNoteText.data(), 1, anchorNoteText.size(), out ); @@ -1019,7 +1033,9 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // R2/N4: the price context (see prPriceDocument) — the envelope and every root attribute that does not // vary per candidate trim level, gathered once. const PrPriceCtx priceCtx{ .g = &g, .sharedAttrs = &sharedAttrs, .anchor = &anchor, .baseEscaped = &escBase, .atAttrs = &atAttrStr, - .envelopeBytes = envelopeBytes, .runClauseBytes = rw::kRunHintLegendClause.size(), + // #219: the run clause is priced through the SAME seam that writes it, so the root + // sentence is charged exactly when it is emitted — never the bare constant's size. + .envelopeBytes = envelopeBytes, .runClauseBytes = rw::runHintClauseIfRows( 1, prRootRelRuns ).size(), .estUnmeasuredClauseBytes = kPrEstUnmeasuredLegendClause.size(), .changedFiles = changed.size(), .skippedModeOnly = skippedModeOnly, .budgetTokens = budgetTokens, .isDefaultBudget = budget.isDefault }; @@ -1056,7 +1072,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest const auto allOwners = gitFileAuthors( root, ing, UINT32_MAX, 182.5, onlyRoot ); // §A9.5 / §P11.4: run= on the named test rows, from the SAME index --affected/--situ/--test-gate read. - const TestRunnerIndex prRunners( ing ); // built once, like coSets/allOwners — the bundle re-renders + const TestRunnerIndex prRunners( ing, root ); // built once, like coSets/allOwners — the bundle re-renders // One-time file→defined-symbols index (in id order == file/line order), so each changed file reads its // symbols in O(1) instead of re-scanning all N symbols (A4-P10). Buckets fill in ascending id order. diff --git a/src/sarif.h b/src/sarif.h index cf55cf21..06f4ec0f 100644 --- a/src/sarif.h +++ b/src/sarif.h @@ -126,18 +126,38 @@ inline const char* sarifLevel( std::string_view sev ) // which strips the same way for the same reason). SARIF wants a plain root-relative URI regardless of // which spelling the caller used, so this normalizes BOTH shapes against `rootPrefix` (the run's root, // trailing '/' already stripped — see rootPrefixOf below) rather than assuming a leading "./". +// Review of #219: the leading-"./" strip used to RETURN, before the prefix was ever tried. That is correct +// for the root "." (where the stored spelling is "./x" and "x" is the answer) and wrong for every other +// relative root: `ripwire ./corp` stores "./corp/test/x.sh", the early return yielded "corp/test/x.sh", and +// pasting that from the root the document declares is `cd ./corp && bash corp/test/x.sh` — rc 127. Both +// sides carry the same optional "./", so both sides drop it FIRST and the prefix comparison runs on what is +// left. Root "." then normalizes to "." , matches no path, and the answer is the "./"-stripped file exactly +// as before — the one case the old early return got right is the one case this keeps byte-identical. +// Second review of #219: the filesystem root is the ONE prefix that IS its own separator, so the shape +// below cannot match it. Under `ripwire /` the stored spelling is "/test/check.sh" and the byte at +// f[ r.size() ] is 't', not '/', so the comparison failed and the ABSOLUTE path was emitted into a +// document whose root= declares every path relative to it — testmap.h's runsAreRootRelative is true for +// any single non-empty root, "/" included, so the document's own claim and its rows disagreed. The extra +// clause strips the single leading slash: the same one-past-the-prefix rule, with the prefix and the +// separator being the same byte. It runs AFTER the general shape, so a non-root prefix keeps exactly the +// behaviour it had, and the "//x" spelling (prefix "/" followed by a real separator) is still answered by +// the general clause. Guarded on size > 1 so a file spelled "/" alone stays "/" and never becomes an +// empty URI — a path that is nothing at all would be a wrong answer, not a shorter one. inline std::string_view rootRelativeUri( std::string_view file, std::string_view rootPrefix ) { - if( file.rfind( "./", 0 ) == 0 ) + const auto dropLeadingDot = []( std::string_view p ) noexcept + { return p.rfind( "./", 0 ) == 0 ? p.substr( 2 ) : p; }; + const std::string_view f = dropLeadingDot( file ); + const std::string_view r = dropLeadingDot( rootPrefix ); + if( !r.empty() && f.size() > r.size() + 1 && f.compare( 0, r.size(), r ) == 0 && f[ r.size() ] == '/' ) { - return file.substr( 2 ); + return f.substr( r.size() + 1 ); } - if( !rootPrefix.empty() && file.size() > rootPrefix.size() + 1 - && file.compare( 0, rootPrefix.size(), rootPrefix ) == 0 && file[ rootPrefix.size() ] == '/' ) + if( r == "/" && f.size() > 1 && f.front() == '/' ) { - return file.substr( rootPrefix.size() + 1 ); + return f.substr( 1 ); } - return file; + return f; } // Normalize a scan root for rootRelativeUri above: drop trailing '/' so the prefix strips cleanly diff --git a/src/situ.h b/src/situ.h index 40bd1de6..6d37fe4d 100644 --- a/src/situ.h +++ b/src/situ.h @@ -19,6 +19,7 @@ #include "gitstamp.h" // r26-stamp Task A: gitstamp::atAttr — the at="[+dirty]" root anchor #include "testmap.h" // §P11.4: TestRunnerIndex / runAttr — the run= hint on a named test row #include "didyoumean.h" // H6: nearestIndexedFileClause — the ONE path near-miss suggester, shared with the MCP arm +#include "siblift.h" // L-D: siblift_detail::dirOf — the ONE "directory of a path" primitive, reused not re-rolled #include "serialize.h" // L2: jsonStr() — writeTestGateReportJson's escaping (self-contained: don't rely on // include-order in whichever TU pulls situ.h in first) #include "pageview.h" // §A3a: the ONE paging/truncation vocabulary — the @@ -355,25 +356,26 @@ inline std::vector declDefPartners( const IngestResult& ing, con inline constexpr std::size_t kSituBlastFilesShown = 8; // section [1] — blast-radius file rows; a raisable DEFAULT inline constexpr std::size_t kSituPartnerRowsShown = 8; // section [3] — co-change partner rows; a raisable DEFAULT inline constexpr std::size_t kSituPartnerFileRowsShown = 4; // section [1] — decl/def partner rows +inline constexpr std::size_t kSituSiblingRowsShown = 8; // section [1] — L-D lexical sibling rows; a raisable DEFAULT // §B12.1 gave this the "showing N of M " form so a reader could see the gap without a second sentence; // C1 F-10 adds the machine half — pageview.h's shown=/total=/capped= spelled in prose, because --situ has no // XML root to carry attributes — and the exact pasteable follow-up. All of it appears ONLY on a cut section: // an untruncated section is byte-unchanged, and no section ever prints capped=0. inline std::string situShowingNote( std::size_t shown, std::size_t rowTotal, const char* rowNoun, - std::string_view nextInvocation = {}, std::string_view extraProse = {} ) + std::string_view nextInvocation = {}, std::string_view extraAttrs = {} ) { if( rowTotal <= shown ) { return {}; } - // ORDER IS THE CONTRACT: prose first, then the machine triple, then `next:` LAST — a pasteable command has - // to run to the end of the parenthetical or a reader cannot tell where it stops. `extraProse` is the one - // section-specific sentence (section [1] pointing at --pr-context's own cap) that used to be spliced in by - // hand at size() - 1, which put it AFTER the command. + // ORDER IS THE CONTRACT: the reading first, then the machine attributes, then `next:` LAST — a pasteable + // command has to run to the end of the parenthetical or a reader cannot tell where it stops. A5: + // `extraAttrs` is the one section-specific fact (section [1] naming --pr-context's own cap), and it is an + // ATTRIBUTE spelled beside the triple rather than the 68 B sentence it used to be spliced in as. std::string note = " (showing " + std::to_string( shown ) + " of " + std::to_string( rowTotal ) + " " + rowNoun; - note += std::string( extraProse ); note += " — shown=" + std::to_string( shown ) + " total=" + std::to_string( rowTotal ) + " capped=1"; + note += std::string( extraAttrs ); if( !nextInvocation.empty() ) { note += "; next: " + std::string( nextInvocation ); @@ -419,6 +421,145 @@ inline std::string situNextInvocation( std::string_view selector, std::size_t ne return verb + " --limit=" + std::to_string( needed ); } +// ── L-D — a changed file's LEXICAL siblings ────────────────────────────────────────────────────────── +// The files that move WITH a changed file are usually its neighbours by NAME, and the caller-walk can reach +// none of them: a header does not call the source that implements it, an .inl is not indexed at all, and a +// harness the graph cannot link (a fixture-built test, a generated main) is reached by nothing. On the frozen +// question set (PLAN_OUTPUT_ROUTING_LOOP §1.5, lesson L-D) two answers were incomplete for exactly that +// reason. So section [1] lists them, and the rule is deliberately the dumbest one that is always right: +// +// SAME DIRECTORY, and the same filename stem — or the stem-partner convention testmap.h already owns +// (_test, test_, Test, _unittest, _spec), so widget.cc names widget_test.cc. +// +// SAME DIRECTORY is load-bearing, not a performance trick: a same-stem file in another directory is a +// NAMESAKE, not a partner (RocksDB has db/version_set.cc and utilities/…/version_set_test.cc that are about +// different things), and listing namesakes would make the block noise on exactly the large corpora it is for. +// +// The candidate population is the crawl's, not the INDEX's: a `.inl`, `.ipp` or `.tcc` partner has no grammar +// in any build, so it never enters ing.files, and it is the sibling a C++ change most often has to edit. Those +// come from ing.crawlSkips.unsupported — whose ROW list is capped even though its count is exact, which is the +// one place this list can be short of the truth and is disclosed as unindexed_rows_floor=1 when it applies. +// +// STATIC BY CONSTRUCTION: no git, no graph, no history window. That is what makes it cheap, and it is also +// why it cannot leak a future commit into an answer about the present. +struct SituSiblings +{ + std::vector paths; // the STORED spelling (relativized by the caller), path-ascending, unique + bool unindexedRowsFloor = false; // the crawl's unsupported-extension ROW list was itself cut +}; + +// dirOf is siblift.h's (the other same-directory lens) and the stem is mention.h's pathStem — both +// primitives already existed, and a third spelling of either is the clone --quality-delta reports. +// One changed file's test: same directory, and the same stem or testmap.h's stem-partner convention. Named +// so lexicalSiblings below reads as the two loops it is (candidates x changed files) rather than four levels. +inline bool isLexicalSiblingOf( std::string_view cand, std::string_view changed ) noexcept +{ + if( cand == changed || siblift_detail::dirOf( cand ) != siblift_detail::dirOf( changed ) ) + { + return false; + } + return mention_detail::pathStem( cand ) == mention_detail::pathStem( changed ) + || isTestPartnerOf( cand, changed ) || isTestPartnerOf( changed, cand ); +} + +// ── the directory index the two loops below became ─────────────────────────────────────────────────── +// Second review of #219 (scalability): lexicalSiblings was two nested loops — every candidate against every +// changed path, with isLexicalSiblingOf re-splitting BOTH paths into directory and stem on each pair — and +// the sibling ROW cap applies only after collection, so it bounded the ANSWER and never the work. That is +// O( ( F + U ) x C ), and C is not small on the changes this block exists for. Measured on the function +// itself, best of 3, on a real llvm-project path population grown to the 182,555-file rung by re-rooting +// whole copies of the tree, interleaved, best of 5: C=500 2.20 s, C=2,000 9.10 s. A --situ that spends +// nine seconds deciding which neighbours to NAME is not a mid-task report. +// +// SAME DIRECTORY is the rule's most selective clause, so the changed paths are indexed by directory once: a +// sorted vector and a lower_bound, never a std::map or std::unordered_map (CONTRIBUTING's container rule). +// Each candidate pays exactly one dirOf and one binary search, and a candidate in a directory nothing changed +// in costs nothing beyond that. Both views point into ing.files[], which outlives the vector built from them. +struct ChangedDirRow +{ + std::string_view dir; + std::string_view path; +}; + +// Sorted by ( dir, path ): the directory is the lookup key, and ordering within a directory keeps the scan +// below deterministic without the caller having to think about it. +inline std::vector changedRowsByDir( const IngestResult& ing, const std::vector& changedFile ) +{ + std::vector rows; + rows.reserve( 64 ); + for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) + { + if( changedFile[f] ) + { + rows.push_back( { siblift_detail::dirOf( ing.files[f] ), ing.files[f] } ); + } + } + std::sort( rows.begin(), rows.end(), []( const ChangedDirRow& a, const ChangedDirRow& b ) noexcept + { return a.dir != b.dir ? a.dir < b.dir : a.path < b.path; } ); + return rows; +} + +// Is any changed path in `cand`'s OWN directory a lexical sibling of it? This NARROWS the candidates by +// binary search; the rule itself is still isLexicalSiblingOf, called rather than restated, so it cannot drift +// from the paragraph that documents it. +inline bool hasLexicalSiblingIn( const std::vector& changedByDir, std::string_view cand ) +{ + const std::string_view candDir = siblift_detail::dirOf( cand ); + const auto first = std::lower_bound( changedByDir.begin(), changedByDir.end(), candDir, + []( const ChangedDirRow& row, std::string_view dir ) noexcept { return row.dir < dir; } ); + for( auto it = first; it != changedByDir.end() && it->dir == candDir; ++it ) + { + if( isLexicalSiblingOf( cand, it->path ) ) + { + return true; + } + } + return false; +} + +// ADDITIVE, deliberately: a file may be BOTH a decl/def partner (symbol identity) and a lexical sibling +// (name), and the header/implementation pair is the commonest case of exactly that. Suppressing the overlap +// was tried and reverted — it removed `widget.h` from "the siblings of widget.cc", which is the one row a +// reader of this block is looking for, to save about 20 B. The two blocks answer two questions, and each +// answers its own whole. +inline SituSiblings lexicalSiblings( const IngestResult& ing, const std::vector& changedFile ) +{ + SituSiblings out; + // The candidate order, the sort and the unique below are untouched by the directory index above, so the + // rows and their order are identical to the two-loop form's. + const std::vector changedByDir = changedRowsByDir( ing, changedFile ); + if( changedByDir.empty() ) + { + return out; + } + const auto consider = [ & ]( std::string_view cand ) + { + if( hasLexicalSiblingIn( changedByDir, cand ) ) + { + out.paths.emplace_back( cand ); + } + }; + for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) + { + if( !changedFile[f] ) + { + consider( ing.files[f] ); + } + } + for( const SkippedFile& sk : ing.crawlSkips.unsupported ) + { + consider( sk.path ); + } + std::sort( out.paths.begin(), out.paths.end() ); + out.paths.erase( std::unique( out.paths.begin(), out.paths.end() ), out.paths.end() ); + // Review of #219: this used to be gated on a NON-EMPTY result, and the emitter suppressed an empty + // block — so when the crawl's 500-row cut removed the only candidate, the report said nothing at all. + // That is the silent zero non-negotiable #3 forbids: the cut is a property of the CANDIDATE LIST, not + // of the answer, so it is recorded whenever the row list was short and the block speaks even at zero. + out.unindexedRowsFloor = ing.crawlSkips.unsupported.size() < ing.crawlSkips.unsupportedFiles; + return out; +} + // Section [1]'s decl/def rows and section [3]'s empty-co-change line, as their own emitters: writeSituation // is already this file's largest function and the quality bar counts what a caller ADDS to it, so a fact that // is nameable gets a name. `pathRel` is the caller's own root-relative spelling, passed in rather than @@ -430,7 +571,10 @@ inline void writeSituDeclDefRows( std::FILE* out, const std::vector +inline void writeSituSiblingRows( std::FILE* out, const SituSiblings& sibs, PathRelStrFn pathRel, const SituPageArgs& page ) +{ + // Review of #219: an empty list is NOT nothing to say when the candidate list itself was cut — that zero + // is a floor, and a floor a reader cannot see is a confident wrong answer. Silence is kept only for the + // honest empty: nothing found, and nothing was hidden from the search. + if( sibs.paths.empty() && !sibs.unindexedRowsFloor ) + { + return; + } + // …and the block does NOT take section [1]'s offset. Review of #219: it did, so `--situ=F --offset=20` + // printed "shown=0 total=9 capped=1" with a next= offering --limit=9 — relief that cannot restore rows an + // OFFSET removed — and --offset=7 dropped six rows silently. This is a small fixed block with a cap and + // --limit, like the decl/def partner rows above it, not a paged listing. + const std::size_t cap = effectiveRowCap( page.limit, int( kSituSiblingRowsShown ) ); + const std::size_t shown = sibs.paths.size() < cap ? sibs.paths.size() : cap; + rw::emitTo( out, " lexical siblings ({}){}{} — same directory and stem as a changed file (its header/impl partner, its test, its .inl): " + "NOT transitive dependents, so they are absent from the list below; lexical and static, never a graph result{}\n", + sibs.paths.size(), + " not_dependents=1", + situShowingNote( shown, sibs.paths.size(), "files", + situNextInvocation( page.selector, sibs.paths.size() ) ).c_str(), + sibs.unindexedRowsFloor + ? " — unindexed_rows_floor=1: the crawl rows at most 500 unreadable-extension files, and it hit that cut here, " + "so a sibling no grammar can read may be missing and this count is a FLOOR" + : "" ); + for( std::size_t i = 0; i < shown; ++i ) + { + const std::string_view rp = pathRel( sibs.paths[i] ); + rw::emitTo( out, " {}\n", std::string_view( rp.data(), rp.size() ) ); + } +} + // F2: the two causes the old "(none, or no git history)" conflated, told apart by the only fact that // separates them — how many commits the window actually contained. inline void writeSituEmptyCochangeLine( std::FILE* out, std::size_t coCommits ) @@ -465,9 +644,15 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // from the document, same as every structured verb's root= attribute). const bool situSingleRoot = ing.realPaths.empty(); const std::string situRootPrefix = situSingleRoot ? rw::sarif::rootPrefixOf( root ) : std::string(); + // L-D names one kind of file the index does not hold (an unindexed .inl sibling), so the relativizer is + // split in two: the STORED-path form is the primitive, and the fileId form is that same call. + const auto situPathRelStr = [ & ]( std::string_view p ) -> std::string_view + { + return situSingleRoot ? rw::sarif::rootRelativeUri( p, situRootPrefix ) : p; + }; const auto situPathRel = [ & ]( std::uint32_t fileId ) -> std::string_view { - return situSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ fileId ], situRootPrefix ) : std::string_view( ing.files[ fileId ] ); + return situPathRelStr( ing.files[ fileId ] ); }; std::uint32_t nChanged = 0; @@ -555,12 +740,13 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges const std::size_t blastShown = blastPage.end - blastPage.begin; const std::string blastNote = situShowingNote( blastShown, affected.size(), "files", situNextInvocation( page.selector, affected.size() ), - "; --pr-context's own per-file blast-radius list is also capped, at 20" ); + " prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too)" ); rw::emitTo( out, " [1] blast radius: {} symbols across {} files transitively depend on these changes{}\n", reach.size(), affected.size(), blastNote.c_str() ); // F3: the decl/def partner FIRST — it is the answer to "what else has to change with this file" that the // dependent-symbol ranking below can never produce, because a header does not depend on its own source. - writeSituDeclDefRows( out, declDefPartners( ing, changedFile ), situPathRel ); + const std::vector situPartners = declDefPartners( ing, changedFile ); + writeSituDeclDefRows( out, situPartners, situPathRel ); { // H5/M15: the same floor + gauge the XML graph verbs mark, in this report's prose — through the SAME // fold, graphGaugeTotals, that graphGaugeAttrXml and graphGaugeAttrJson go through. PR #72 (382e66e6) // introduced that fold in the same commit that widened the gauge to three, precisely to stop the two @@ -570,6 +756,9 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges const auto [gaugeAmb, gaugeUnresolved] = graphGaugeTotals( g.ambOut, g.unresolvedOut ); rw::emitTo( out, kGraphCountFloorTextLine, gaugeAmb, gaugeUnresolved, graphUnindexedTextClause( g.unindexedFiles ).c_str() ); } + // L-D: the lexical neighbours of the changed files, which the caller walk above can never reach. + const SituSiblings situSibs = lexicalSiblings( ing, changedFile ); + writeSituSiblingRows( out, situSibs, situPathRelStr, page ); for( std::size_t i = blastPage.begin; i < blastPage.end; ++i ) { const std::string_view rp = situPathRel( affected[i] ); @@ -592,14 +781,32 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // C1 F-10: this listing had a 25-row cap and no relief. It is the ANSWER — the rows you run, the rows // --test-gate exits 4 on — so it is served whole and carries no showing-note at all: there is nothing to // disclose when nothing can be dropped. - rw::emitTo( out, " [2] tests to run ({}){}", tests.size(), - tests.empty() ? ": (none transitively reach these files)\n" - : " — evidence order: [changed] you edited it, [partner] named after a changed file, then hops (1 = calls a changed symbol directly); " - "a line (n): a, b lists n files sharing that evidence with no derivable runner:\n" ); + // A5: order=evidence is the SAME attribute --affected's root carries for the same ordering, so the two + // verbs name it identically; what follows is the reading of the tags the rows themselves print, which has + // no attribute form and therefore stays as the shortest sentence that defines them. + // A3 / third review of #219: the trailing clause named `root:` UNCONDITIONALLY. A multi-root report + // declares no `root:` at all (see situSingleRoot above, which gates that line) and TestRunnerIndex + // correctly keeps the absolute command there — so on that report the sentence pointed at an anchor the + // reader could not find. That is the same defect this lane fixed in the shared run-hint clause, having + // survived in this dialect's own heading because the heading is prose rather than the shared constant. + // Both spellings now answer to the ONE predicate that also decides how the command is spelled + // (testmap.h runsAreRootRelative, the same one TestRunnerIndex is constructed with), so the sentence and + // the command cannot disagree. The order half is shared between the two forms rather than duplicated, + // and the single-root form is byte-identical to what it was. + static constexpr std::string_view kSituRowsOrder = + " order=evidence: [changed] you edited it, [partner] named after a changed file, then hops asc (1 = direct); " + "\"(n): a, b\" = n runner-less files sharing that evidence; "; + static constexpr std::string_view kSituRunRootRel = "a (run: …) is relative to root:\n"; + static constexpr std::string_view kSituRunAbsolute = "a (run: …) is absolute: this report spans several roots, so there is no one root to be relative to:\n"; + const std::string situRowsHeading = + tests.empty() ? std::string( ": (none transitively reach these files)\n" ) + : std::string( kSituRowsOrder ) + + std::string( rw::runsAreRootRelative( ing, root ) ? kSituRunRootRel : kSituRunAbsolute ); + rw::emitTo( out, " [2] tests to run ({}){}", tests.size(), situRowsHeading.c_str() ); // §P11.4: this section says "tests to run" and named files that are not commands. The runner is appended // where one is DERIVABLE and omitted where it is not — see testmap.h; a guessed command is worse than none. // E1: runner-less rows with equal evidence are ONE `[hops=N] (n): a, b` line — testmap.h's seam, the multiset unchanged - const TestRunnerIndex situRunners( ing ); + const TestRunnerIndex situRunners( ing, root ); rw::emitRaw( out, testRowsJoined( situRunners, evidenceRowsOut( testRows, EvDialect::Text, situPathRel ), TestRowShape{ RowDialect::Text, {}, " " }, []( std::string_view s ) { return std::string( s ); } ).c_str() ); // §B7.3: this section inherits --affected's blind spot without --affected's disclosure — a shell harness @@ -607,8 +814,8 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // named above, however much of the change it exercises. Same number, same counter as --affected's // script_gates_unmodelled= (testmap.h), because it is literally the same blindness on the same traversal // — and it matters MOST on the empty listing above, which otherwise reads as "nothing tests this". - rw::emitTo( out, " ({} 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)\n", + rw::emitTo( out, " script_gates_unmodelled={} — test/*.sh gates never appear above: script-to-binary " + "edges are not call edges (a path count)\n", scriptGatesUnmodelledCount( ing ) ); // (3) co-change partners NOT in the diff — "you usually edit these together; did you forget?" @@ -691,6 +898,7 @@ struct SituationFacts // Carried on the FACTS, not re-derived per surface, so the CLI report, the MCP JSON and --handoff cannot // disclose it three ways or two of them forget. std::vector declDef; // F3: files defining the SAME (scope, name) symbols as the changed set — the header/impl pair the transitive list cannot reach + SituSiblings siblings; // L-D: same-directory, same-stem neighbours of the changed files — STORED spellings (an unindexed .inl has no fileId) std::string coWindow; // the window label its co-change was mined in ("18mo@HEAD"), empty only if never mined std::size_t coCommits = 0; // commits that window actually contained — 0 ⇒ the zero above is not a measurement std::vector> hotspots; // (changed file, cx×churn score) for high-risk changed files (score desc, path asc) @@ -773,6 +981,7 @@ inline SituationFacts computeSituationFacts( const std::string& root, const Inge // popen per probed file — up to 40 — the O(files)-subprocess storm). Deterministic for a fixed HEAD. const auto coSets = gitCommitFileSets( root, ing, "18 months ago", 30 ); facts.declDef = declDefPartners( ing, changedFile ); // F3: same relationship, same rule, one implementation + facts.siblings = lexicalSiblings( ing, changedFile ); // L-D: the same list the CLI report's [1] prints facts.coWindow = defaultWindowLabel( root, "18mo" ); // F2: the composed zero's window travels WITH the zero facts.coCommits = coSets.size(); HashMap partnerDeg; @@ -1153,12 +1362,12 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const // H2H-Graft F1: the evidence clause (testmap.h's ONE wording) rides the rows-gated half, like the run= rule. rw::emitTo( out, "{}", kTestGateLegend, tgHasRows ? kTestGateRowLegend : "", std::string_view( kTestRowEvidenceLegend.data(), tgHasRows ? int( kTestRowEvidenceLegend.size() ) : 0 ), - runHintClauseIfRows( testRows ), // the ONE gate: this clause is about rows, so an untested-only report pays nothing + runHintClauseIfRows( testRows, runsAreRootRelative( ing, root ) ), // the ONE gate: this clause is about rows, so an untested-only report pays nothing rw::graphUnindexedLegend( g.unindexedFiles > 0 ), // #66: exactly when the root carries the attribute rw::rootRelPathsLegend( !tgRootAttr.empty() ) ); // §P11.4: this gate EXITS 4 on the obligation, so its rows carry the command that discharges it — where // one is derivable. Absent run= = not derivable (testmap.h states why a fallback would be a lie). - const TestRunnerIndex gateRunners( ing ); + const TestRunnerIndex gateRunners( ing, root ); // shown_tests= / tests_capped= are DERIVED from the rows this document actually emits, not asserted. // tests_capped= was the string literal "0" — a disclosure that could never become "1", so if a row // cap were ever added the attribute would keep saying nothing was cut while something was. It is kept @@ -1229,7 +1438,7 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co const std::size_t testRows = r.tests.size() + r.shellGates.obligations.size(); // same rows-gate as the XML twin, so the two dialects disclose the SAME facts about the same run rather // than one carrying a root the other omits (test/mcpclidiffcheck.sh's parity question). - const TestRunnerIndex gateRunnersJ( ing ); // P3 (L7): the root's next= needs the runner index before the rows + const TestRunnerIndex gateRunnersJ( ing, root ); // P3 (L7): the root's next= needs the runner index before the rows const bool tgJHasRows = ( testRows > 0 || !r.untested.empty() ); const std::string tgJRootJson = ( root.empty() || !tgJHasRows ) ? std::string() : ( ",\"root\":\"" + jsonStr( root ) + "\"" ); // The XML twin's derived pair, mirrored key-for-key: "tests_capped":false was a literal here too. @@ -1245,7 +1454,7 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co graphCountFloorAttrJson( g ).c_str(), // M15: the JSON twin's gauge + "counts_floor":true rw::cstr( pageJson ), atJson.c_str(), tgJRootJson.c_str(), // M12: root= rides only when the document has rows (same gate as the XML twin) nextFieldJson( testGateNextInvocation( ing, r, gateRunnersJ ) ).c_str() ); // P3 (L7): the XML twin's next= - const TestRunnerIndex gateRunners( ing ); // §P11.4, the JSON sibling of the XML run= + const TestRunnerIndex gateRunners( ing, root ); // §P11.4, the JSON sibling of the XML run= const auto jesc = []( std::string_view s ) { return jsonStr( s ); }; rw::emitRaw( out, testRowsJoined( gateRunners, evidenceRowsOut( r.testRows, EvDialect::Json, tgJPathRel ), TestRowShape{ RowDialect::Json, "p" }, jesc, "," ).c_str() ); // E1: the XML twin's , "p" an array for( std::size_t i = 0; i < r.shellGates.obligations.size(); ++i ) diff --git a/src/testmap.h b/src/testmap.h index 710576c7..d48be47b 100644 --- a/src/testmap.h +++ b/src/testmap.h @@ -26,6 +26,8 @@ #include "docparse.h" // docparse::detail::readWholeFile — the canonical whole-file byte read (reused, not re-rolled) #include "mention.h" // mention_detail::baseNameOf + stripExt — the ONE basename/stem pair binstale.h/gitmine.h reuse #include "infra/namesplit.h" // namesplit::isIdentChar — the canonical ASCII identifier-byte predicate +#include "sarif.h" // rootPrefixOf / rootRelativeUri — the ONE relativizer every p= emitter already shares (A3) +#include "infra/jsonesc.h" // rw::shSingleQuote — the ONE shell quoter; run= is a COMMAND, see spell() below #include #include @@ -486,10 +488,25 @@ inline std::vector exercisedSymbols( const IngestResult& ing, const Grap // // COST: the candidate scripts' texts are read at most ONCE per invocation and only LAZILY — nothing is read // until a row actually asks for a hint, so every verb that emits no test row pays nothing at all. +// A3 / review of #219: run= is spelled relative to root= exactly when the run HAS one root and declares it. +// A multi-root run's disk path lies under no single root, so its command must stay absolute — and the legend +// sentence below is gated on this SAME predicate, so the spelling and the claim cannot disagree. +inline bool runsAreRootRelative( const IngestResult& ing, std::string_view root ) noexcept +{ + return ing.realPaths.empty() && !root.empty(); +} + class TestRunnerIndex { public: - explicit TestRunnerIndex( const IngestResult& ing ) : ing_( &ing ) + // A3 (one absolute root per document): `root` is the run's own crawl root, and its ONLY use is to spell + // the command below relative to it — the same rootPrefixOf/rootRelativeUri pair every p= emitter uses. + // Defaulted to "" so a caller that has no root (or a multi-root run, where the disk path is not under any + // single root) keeps the absolute spelling: an unrelativizable command must stay pasteable, never become + // a path relative to a root that does not contain it. + explicit TestRunnerIndex( const IngestResult& ing, std::string_view root = {} ) + : ing_( &ing ), + rootPrefix_( runsAreRootRelative( ing, root ) ? rw::sarif::rootPrefixOf( root ) : std::string() ) { for( std::uint32_t f = 0; f < std::uint32_t( ing.files.size() ); ++f ) { @@ -517,6 +534,10 @@ class TestRunnerIndex return cache_.emplace( fileId, derive( fileId ) ).first->second; } + // Whether the commands this index spells are relative to a root — the SAME fact the legend sentence + // is gated on, read off the index rather than re-derived at each legend site. + bool rootRelative() const noexcept { return !rootPrefix_.empty(); } + std::string commandForScript( std::uint32_t fileId ) const { return fileId < ing_->files.size() && runnerVerb( ing_->files[fileId] ) != nullptr ? spell( fileId ) : std::string(); } @@ -540,8 +561,7 @@ class TestRunnerIndex // primitives binstale.h, gitmine.h and docdrift.h already stem paths with. Re-rolling them here is // exactly the new-clone-of-a-reused-helper --quality-delta reports, and it would also fork the // "strip the LAST dot" convention that every other stemming call site in this repo shares. - static std::string_view stemOf( std::string_view p ) noexcept - { return mention_detail::stripExt( mention_detail::baseNameOf( p ) ); } + static std::string_view stemOf( std::string_view p ) noexcept { return mention_detail::pathStem( p ); } void loadTexts() const { @@ -593,17 +613,72 @@ class TestRunnerIndex // Spelled against the ON-DISK path (diskPath), so a multi-root `