diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 0b5e8c75e..06ac7ac6f 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -8,6 +8,7 @@ ack api-surface 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 M12 (capture-audit L9): 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 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 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. @@ -35,7 +36,7 @@ ack api-surface 2e6026bd58111ad5 5 cid=3d6b010d178e2c8f by=src/* lane/n6-d, the ack api-surface 30dfe3580e3235a8 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 32e780668c108fa5 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 33d55f3b93bc79ea 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 3478654139c90f0f 4 cid=789faaec74b2ce26 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 3478654139c90f0f 5 cid=fc086b8b559ee8c6 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 3561d0281d324276 14 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 3703c22e2f2112bd 5 cid=e07b67ecf3068d1d by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack api-surface 3877dd1e9b4ae997 4 R-E (2026-08-17 harvest): narrowLegoToRenderedSigs needs an explicit rootPrefix param because packSignatures' sigsRendered rows are already root-relative while the function's own escaped path comparison was still absolute -- every comparison silently failed, narrowing legoScoped to nothing on every --for run whose rendered sigs hit this path. The +1 param (defaulted, so every other caller is unaffected) and the small cx/LOC bump on narrowLegoToRenderedSigs and its one caller runForLens are the minimal fix; caught by legobundlecheck.sh going red for the wrong reason. churn=self is this lane's own edit window. @@ -392,7 +393,7 @@ ack complexity 4b8467d6559779e4 22 fnbody-require lane: deliberate additive API ack complexity 4c787f37a66f10cc 75 cid=c646b4cc29e9de1c 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 complexity 4d421276056367a9 18 cid=5a1a2f3caf33a19e 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 complexity 50771ea7eb2ce242 19 cid=c2388456093ea895 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 -ack complexity 52c2e3e32753dd28 154 cid=2783212e28944a4b round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch | 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 complexity 52c2e3e32753dd28 161 cid=32fe671bf177bea8 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch ack complexity 5361e2bced6f1988 23 cid=b34695b5d2051f1d 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 | prior: M12 follow-up (capture-audit L9): --ensemble gained root=/root-relative p= — writeEnsembleReport's 3 new default-valued params (singleRoot/rootPrefix/rootAttr, back-compat) thread the caller's already-computed single-root spelling through; short-horizon-churn on the touched dispatcher. ack complexity 53a1214d5505fa41 433 cid=e155c3fb2ceeb9f1 lane V1 N4 (bd8d666): the no-baseline half of the M8 --since policy decided beside the shape/range validation, from SinceScope::baselineSha | prior: 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 ack complexity 53b3f823688054b4 83 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). @@ -484,6 +485,7 @@ ack duplication 0097b5800ff95c8e 304 Chip-trio merge 2026-08-23, finding 1 of 4. ack duplication 00b0c885f543c639 50 by=test/macroreparsefix lane fix/cpp-macro-member-reparse-2026-09-11, member-macro re-parse fixtures (test/macroreparsefix): sumDraft (leak_plain.cpp) and tallyCargo (leak_anon.cpp, control/semi.cpp) are deliberately ordinary C loops around the member macros, and test/macroreparsecheck.sh pins them BY NAME as the functions a derailed parse loses, so their bytes are the measured input. Their token streams meet unrelated plain loops (abicheck KindCounts::sumWhere, vendored dynamic_map.hpp node_rank::lt/le, bench geometry perimeter, the cloneidiomfix and sliceflowsensfix fixtures) with no shared identifier or contract; rewording a fixture to dodge a token match would game the number. ack duplication 03945adfda18f84d 20 cache-pack round, reviewed each: fixture functions deliberately mirror common loop shapes (token-matches to unrelated helpers are the fixture doing its job); needsLoopContext is a 3-clause tag predicate shape-matching other tiny predicates; mergeCachePack parallels mergeAtomsPack by documented same-shape contract ack duplication 03a66775dbf27f15 78 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 07acd981dd8a9619 36 E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. ack duplication 094dbeec3d6a6f8f 32 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 0aba5feb0affb13a 43 by=test/macroreparsefix lane fix/cpp-macro-member-reparse-2026-09-11, member-macro re-parse fixtures (test/macroreparsefix): sumDraft (leak_plain.cpp) and tallyCargo (leak_anon.cpp, control/semi.cpp) are deliberately ordinary C loops around the member macros, and test/macroreparsecheck.sh pins them BY NAME as the functions a derailed parse loses, so their bytes are the measured input. Their token streams meet unrelated plain loops (abicheck KindCounts::sumWhere, vendored dynamic_map.hpp node_rank::lt/le, bench geometry perimeter, the cloneidiomfix and sliceflowsensfix fixtures) with no shared identifier or contract; rewording a fixture to dodge a token match would game the number. ack duplication 0cbdfdcac9c7a134 29 wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD @@ -536,6 +538,7 @@ ack duplication 68b1751fdaff3631 19 lane fix/cpp-macro-member-reparse-2026-09-11 ack duplication 6a7680fbd8a5083d 20 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 6c20f55a7fbeda1b 41 lane E close (terminality round A): suite fixture data (bench/agentloop/editsuite/fixture/geometry.cpp) shares distance's body with test/fixture/geometry.cpp; the fixture is byte-frozen measured data (baseline + post-fix tables) ack duplication 6df9a7992177fa30 30 by=src/extentsuspect.h lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. +ack duplication 73b08512b41c61f2 101 E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. ack duplication 7589afcf46d892d1 38 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. ack duplication 767f5200ffbdb095 54 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack duplication 77278c286d945470 22 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. @@ -605,6 +608,7 @@ ack duplication eb2e912436eacba2 49 by=test/macroreparsefix lane fix/cpp-macro-m ack duplication ec3f7b5523723637 110 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 duplication ef3d5272b705b505 107 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 ef69b96712422a31 36 by=src/extentsuspect.h lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. +ack duplication f10ce50bdc680d80 122 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. ack duplication f14cfce02be351ad 24 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 f3518ef93569f6ae 25 idiom-class clone false positive: a three-way ternary over string literals, 25 normalized tokens, sharing no domain identifier with macroRoleAttr and in an unrelated subsystem. Reading both confirms it. ack duplication f4f3fc233cdda6ba 307 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) @@ -644,6 +648,7 @@ ack new-clone-of-reused-helper 675204169bc3d5d2 4 wave-3 close, H7 hosts (substr ack new-clone-of-reused-helper 689ab18d529fb67c 4 P3.2 plan-lint model correction (validated against the field-evidence source repo's own flagship plan): computePlanLint/writePlanLint grow in complexity/verbosity because status now resolves from the §Status ledger as well as the card body (the real house dialect), extracted into resolveLedgerStatus to keep the growth to the genuinely new logic; missingWhy clones gitmine::coPairAttr and testmap::affectedSeededBy — the same trivial-classifier-idiom false positive already acked for glyphName/shapeName (a 3-branch string-constant selector has almost no room to look structurally unlike another one) ack new-clone-of-reused-helper 6a7680fbd8a5083d 4 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack new-clone-of-reused-helper 6df9a7992177fa30 4 by=src/extentsuspect.h lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. +ack new-clone-of-reused-helper 73b08512b41c61f2 4 E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. ack new-clone-of-reused-helper 7981b72e3b2a8a8b 3 mcpIsGitOnlyVerb (finding #8, 2026-08-15 harvest) is a 6-line linear-scan-and-compare over the SAME small kMcpGitOnlyVerbs table mcpGitOnlyNotBatchServed already loops beside it (mcp.h:127-144); query.h::isKnownLayerWord is the same trivial 'is w one of these known short strings' idiom over an UNRELATED domain (the graph-query layer vocabulary). Per this repo's own precedent (V6/isKnownVerb, ROUTING_LEDGER.md's V6 lane), forcing a shared cross-file generic membership helper across two unrelated small closed vocabularies is the wrong abstraction for a token-shape match, not a real duplication to collapse ack new-clone-of-reused-helper 845088dc7642af45 4 V3 harvest 2026-08-15: two MCP gate harnesses. Every MCP gate in this suite is deliberately STANDALONE — the house rule in their own headers is 'does NOT edit regression.sh or any other existing test file', so a gate carries its own 3-4 line JSON-RPC transport wrapper (mcp_call in mcphandlecheck, mcpCall in mcpeditpresencecheck, the curl wrapper in mcpremotecheck). test/mcptoolprunecheck.sh needs BOTH transports (HTTP for the pinned-root arms A-E/G, stdio for arm F, and the pair is the point: pinning is exactly what makes the omission provable), so its http_call and stdio_call land as the 9th and 10th members of two families that already exist. Extracting a shared test/lib harness would couple every MCP gate to one file and is a suite-wide refactor, not this lane's; sharing one of the two existing spellings instead would make this gate fail whenever an unrelated gate edits its own helper. No production code involved ack new-clone-of-reused-helper 894d5d3469e376e9 3 M1: four MCP gates now share a call() helper that injects legend:"full" for the seventeen declaring verbs. The clone is DELIBERATE and this repo's gates say why in their own comments: two gates deriving the same fact through one shared helper fail together on a shared mistake, so gate helpers are copied, not factored. Each copy carries its own re-pin note explaining the posture that gate needs. @@ -665,6 +670,7 @@ ack new-clone-of-reused-helper de772ce9497e6cc2 7 test/verify_strkern.cpp keeps ack new-clone-of-reused-helper dec50094e5aef506 3 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 new-clone-of-reused-helper e6ae1365cb6c154a 3 V3 harvest 2026-08-15: two MCP gate harnesses. Every MCP gate in this suite is deliberately STANDALONE — the house rule in their own headers is 'does NOT edit regression.sh or any other existing test file', so a gate carries its own 3-4 line JSON-RPC transport wrapper (mcp_call in mcphandlecheck, mcpCall in mcpeditpresencecheck, the curl wrapper in mcpremotecheck). test/mcptoolprunecheck.sh needs BOTH transports (HTTP for the pinned-root arms A-E/G, stdio for arm F, and the pair is the point: pinning is exactly what makes the omission provable), so its http_call and stdio_call land as the 9th and 10th members of two families that already exist. Extracting a shared test/lib harness would couple every MCP gate to one file and is a suite-wide refactor, not this lane's; sharing one of the two existing spellings instead would make this gate fail whenever an unrelated gate edits its own helper. No production code involved ack new-clone-of-reused-helper ef69b96712422a31 3 by=src/extentsuspect.h lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/extentsuspect.h): extent::inSet is the std::find( begin, end, v ) != end membership test over a constexpr std::array of enums (the SymKind and Lang rule tables, 9 call sites). Its partners spell the same one-liner over a span of string_view (ingest_relations.h namesNode), an initializer_list of string_view (naminglens::ncAnyOf) and a Scope pattern vector (quality::scopeUsesDiffToken), and notes::sortNotes is a bare std::stable_sort. An idiom collision at 30-36 tokens across four subsystems: no member can take another member argument type, and the comment on namesNode records this spelling as the house one. +ack new-clone-of-reused-helper f10ce50bdc680d80 6 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. ack new-clone-of-reused-helper f5a96185fd557a83 3 R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. ack new-clone-of-reused-helper ff14ff2b821304c8 4 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack params 00864cc4801c708a 6 cid=88b8c4067d691df7 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. @@ -739,15 +745,15 @@ ack short-horizon-churn 0e08ebeb77eaf664 8 W1-S2 churn-keying fix (pathQualified ack short-horizon-churn 1039e3c8e0fc3667 21 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 short-horizon-churn 105c48e20c80c896 3 cid=720fab31ea99ebde A2: short-horizon churn on resolveOneForEdit and unmeasuredHintNote is this fix round itself. Both symbols were touched by 4db6fb3 (A1: the NUL-payload refusal and the never-parsed disclosure) and again here (A2: the absolute-hint frame). churn=self, three commits in one lane on the surface the lane was assigned; it is not instability in the code, it is the audit being executed one item per commit as instructed. ack short-horizon-churn 106ba93f707963e4 6 cid=f64a7a84df923458 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn 1085f731a3dde7c8 7 cid=b89f92c2b2936f1f M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. +ack short-horizon-churn 1085f731a3dde7c8 33 cid=11e32cce5f112d11 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn 109200616e8e6d9e 3 cid=bea14e1133ee095b ingest-path disclosure: eval prints ingest: lex=rich|scan and --doctor prints rich_verbs= derived by asking needsValueUses (now ONE function in cli.h) per verb. short-horizon-churn on runEvalRetrieval is this session's repeated edits to it, not instability; mutation control in knownitemcheck proves both new arms fail when the eval verbs leave the predicate. | prior: rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn 10f47dd5a3f35d86 33 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 111000c203e47c35 5 cid=12c85505363fa80e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 1251aefc21a466dc 6 cid=813f4fac69d7378d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 1273fad87a99a9f2 6 cid=e6e7ad831a784a6c M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. ack short-horizon-churn 129d3c8d5a763870 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. -ack short-horizon-churn 131068a6cedf0864 10 cid=bef0a5079a2bf8e4 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit -ack short-horizon-churn 15061a69cb5b451f 4 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack short-horizon-churn 131068a6cedf0864 18 cid=6df1f8794b53f5a2 E1 follow-up (#214 CI): prLegendText/writePrContext were rewritten by this PR's own first commit (the clause splice) and again here to gate it on the corpus holding a test file; one change, two commits | prior: E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself +ack short-horizon-churn 15061a69cb5b451f 25 cid=c11a1cfb11d0325d E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn 1520fa02411735c3 4 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 15754e3561a34f40 39 cid=93c7392c7b677557 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack short-horizon-churn 1610c5acaa7d4806 6 cid=545658032a875c30 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -813,7 +819,7 @@ ack short-horizon-churn 3298be65bf6058ef 100 cid=0d8a43a6b93b2f31 M1: self-churn ack short-horizon-churn 32d067796a0393b8 32 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn 32e780668c108fa5 33 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 3385856c74077f1a 22 cid=ef5ec129660c61f5 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. -ack short-horizon-churn 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack short-horizon-churn 3478654139c90f0f 7 cid=fc086b8b559ee8c6 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3561d0281d324276 29 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. ack short-horizon-churn 357ab167dccb9a4d 2 cid=ebe308d04e543d16 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn 3703c22e2f2112bd 7 cid=e07b67ecf3068d1d 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) @@ -831,7 +837,7 @@ ack short-horizon-churn 3be1c13661e5a63c 5 pack-task budget round (verifier K1+K ack short-horizon-churn 3c06a3d024349e5f 5 cid=f88a6b24c26cb6b9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3c07d993bfdbce53 8 cid=3718024f74a80d86 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked | prior: lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack short-horizon-churn 3c7a8e2ee4351734 11 fix-grep lane 2026-08-15: this symbol's ONLY change is the boolean line-scope branch swapping the 512-byte-capped DISPLAY helper grepMatchedLine for the new uncapped grepWholeLine. That cap was the bug in both directions (a required --and term past byte 512 dropped a real hit; a forbidden --not term past byte 512 failed to exclude its row), so churn=self here IS the fix and nothing else. Proven by an independent oracle: test/grepandcheck.sh (3a)/(3b)/(3c) now derive truth from /usr/bin/grep over a fixture whose second term sits at ~col 640, and all three arms are RED against the pre-fix binary and green after. Identity restored on this repo: --grep=stale --and=stale 289 -> 292 = plain --grep=stale; --grep=symbol --and=symbol 3028 -> 3030; the five mcp.h sites at cols 513-790 the verifier named are back -ack short-horizon-churn 3d87404c1cdf50ec 88 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack short-horizon-churn 3d87404c1cdf50ec 118 cid=7f345cc46d2fd3d8 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3e0a074094789d60 15 cid=1556a2e06eef44c3 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 3e7c1221865b5b52 13 cid=f636d73c8428270e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3e91cee9f02f22a2 12 cid=76f4b09ef77556bd R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. @@ -854,6 +860,7 @@ ack short-horizon-churn 472cc93317130a8b 6 cid=dcdfa60eb93788ed OPTREMARKS F3 (d ack short-horizon-churn 476ab6f670e5d871 13 cid=bb27e8c78edbdb2f OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 48c3932de16d9cdd 9 cid=51e0c2d620ad5d17 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack short-horizon-churn 4975dcbd128411d7 5 cid=9b40c150a9616ae3 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 49a806e993ebe684 11 cid=fae3fb658b95ef51 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn 49e172c2aa455e68 4 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 49e838ca0844258b 5 cid=9d23ed0d32fea19d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 49ea9954a4e77a06 2 cid=ad0e2ee1266299ec selector-parity + degraded-routing round (2026-08-30): churn=self on emitGrepReport and classifySkipHealth is this one edit window — each was edited twice within the round because the round's own quality-delta demanded the second pass (the parse_degraded= inline predicate and classifySkipHealth's errNodes test both re-routed through the ONE fileParseDegraded predicate hoisted to model.h, so the three degraded surfaces cannot drift). Final state carries no complexity/verbosity/duplication finding; selectorscopecheck 8 arms + degradedhintcheck 8 arms green. @@ -872,7 +879,7 @@ ack short-horizon-churn 4f90e22ea95dea67 22 cid=fe301df9e2f3c6db by=src/* rung 3 ack short-horizon-churn 4fb850ad3a3bb9c3 5 cid=efacb2a5f9e0435a OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 4fc8bad5056ad3f3 7 recall.h is the intended edit target for lane L4 (structure-aware doc truncation); truncateRecallBody and buildSectionGranularBody are the two functions the task names, and the file is a legitimately hot area (many prior feature rounds) — churn is inherent to the assignment, not a code-shape defect to fix ack short-horizon-churn 5274ab1a5e193cb4 7 cid=5cfc96d7c5424d21 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn 52c2e3e32753dd28 10 cid=2ffb98cdf6477093 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit | prior: F4 (lane F): churn=self on writePrContext is this session's own two edits to that symbol inside one window while the empty-root tail converged (inline first, then extracted to prEmptyRootTail), not accumulated debt — the extraction is why the complexity and verbosity rows the inline version carried are gone. +ack short-horizon-churn 52c2e3e32753dd28 20 cid=32fe671bf177bea8 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: E1 follow-up (CodeRabbit on #214, threads 1-3): the pr-context legend must follow the RENDERED rows, so the chosen body is now rendered before the head is written. prRenderLevel NAMES the memstream dance pickPrTrimLevel already ran inline and the unbudgeted root now needs as well - two call sites, one copy - and the clone it matches is packtask.h's packTaskRenderToString, the same seven-line idiom this tree already spells nine more times in mcpverbs.h; hoisting one shared helper into serialize.h would collapse three DEGRADED_PATH_ALERT messages that deliberately say WHICH buffer failed, so the duplication is the cheaper honesty and is recorded here rather than papered over. prBodyHasTestRow is a two-find predicate (a test row opener or a group row opener); its 36-token match against accessshape::chaseTypeCanPoint is the shape find(x)!=npos || find(y)!=npos, not shared logic. writePrContext's churn=self is this lane's own third edit to that one function inside the window - the E1 landing, the CI budget fix, and this rows-gated legend - one change seen three times, none foreign. ack short-horizon-churn 52de3cd8a58763d1 7 cid=462928d2141114f5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 5383f63ad718935c 8 cid=c7781d76aeed7d13 R8 printf-family pilot: std::printf/fprintf/snprintf -> std::print/std::format across every call site in verbs_lint.h, a file independently hot this round; converting a printf call necessarily rewrites the same recently-touched line, so self-thrash is inherent to the conversion, not new debt. Byte parity verified via test/printffmtparitycheck.sh (RED on an injected 1-byte mutation, GREEN on this pilot). ack short-horizon-churn 5391ffd9aa5765bf 44 W1-S2 dead-code top-level fix: isDeadCandidate param add + the edits to computeDelta/computeSnapshot are the fix itself @@ -902,7 +909,7 @@ ack short-horizon-churn 61d6cde8defa73ad 7 cid=5d0a21a4321d8300 OPTREMARKS F3 (d ack short-horizon-churn 61e5df9e1e40ff70 13 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack short-horizon-churn 623e9c51c095e307 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 624a465290b8a040 3 cid=7b2469337b9ad141 lane E close (terminality round A): run_editsuite.py: the ripwire half split into classify_ripwire_call; remaining rows are churn on the change's home -ack short-horizon-churn 6302e2e27e23bcde 4 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 short-horizon-churn 6302e2e27e23bcde 7 cid=30a2424bb2bbbdf5 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn 639de1c3670999f9 30 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack short-horizon-churn 649ed79cefcd6fc6 6 cid=a3636823a3a67d9f by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn 6652d5114718eb63 7 cid=497a477af2561c0a 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. @@ -924,6 +931,7 @@ ack short-horizon-churn 7331622b497469cf 2 cid=3594ad7d1f7d9cbd fix-round follow ack short-horizon-churn 733665960f2dc458 35 cid=085fdb748171c083 by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn 73ac37ff2580b0b0 103 cid=b07db8431c1b7c18 R1 identity round (harvest Wave-2 R1, 2026-08-24). Five gating rows, ONE change: finding identity now survives a rename or a move. (1) runQualityDelta complexity 201->205 and verbosity 419->462: the +43 lines are overwhelmingly the LEGEND this repo requires — the identity mechanism is a claim about what an ack means, so the report has to say what it rested on (both mechanisms, both truncation caps, the two per-route counters), and legendcoveragecheck arm (A) requires every new attribute be named there. The code added to this dispatcher is three lines: the ledger is now taken from the already-healed basis instead of re-read (re-reading it here would undo the healing), one countAckRescues call, and one call to quality::identityDisclosure — which is itself an EXTRACTION made in this lane precisely because --quality-delta flagged the inline version. The residual +4 complexity is the cid ternary at the ack-write site. Decomposing runQualityDelta is its own round, recorded as such by the W2-E ack above and unchanged by this one. (2) resolveDeltaBasis verbosity 73->86 + churn=self: the two healIdentity calls (ref-pair and working-tree arms) and the comment explaining why the healing must precede computeDelta — heal the baseline after the delta is taken and the delta has already been computed against the stale identity. churn=self on it and on mergescout::injectFileLevelFallback is this one edit window, not a history signal. (3) injectFileLevelFallback: its hand-rolled fopen/fseek/fread/fclose block was retired onto docparse::detail::readWholeFile, the canonical whole-file read, after this lane extracted quality.h's copy and the reuse-decline kind reported the pair as a 320-token clone. Re-read before the swap: short read or ftell failure clears the buffer and the file contributes nothing, byte-identical to what it replaced. Gate: test/identitycheck.sh, 19 arms, 9 RED on the pre-lane binary. ack short-horizon-churn 763ff87870373241 102 cid=c8f6829a03ecca15 A4: short-horizon churn on runCliEdit is this fix round itself -- the lane touched it for A1 (the NUL-payload arm) and again here (the resolved-identity advice), one commit per assigned item. churn=self, not instability in the code. +ack short-horizon-churn 765e0d29d1478ba2 6 cid=b2e777292ce742d0 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn 775b773b1a3d2349 6 cid=ae06371e2a89c87b rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: 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. ack short-horizon-churn 775cfb65ec71343a 4 cid=b9f27a565025cb61 wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file ack short-horizon-churn 779387a679944627 5 cid=b026f45951caaa1d 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. @@ -945,6 +953,7 @@ ack short-horizon-churn 80221b58c708d30c 5 cid=1bcf5c4c17466d1b OPTREMARKS F3 (d ack short-horizon-churn 802417eb4f7f22a6 5 cid=bd29ae0cb0947813 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 80a2f3a7c92fbed4 7 cid=df52b38ce1ca6bb9 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn 80b08c75913ae76c 2 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 short-horizon-churn 813b954f28ce86af 31 cid=5d418e33b2f1f9d5 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn 815fbf65eea5df47 5 cid=98131c6d45bba16a OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 8192a44ad5eb2510 3 cid=ff366a47a1cdb76d by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 81efad81c80fc1cd 10 cid=99f4094a45b32fa2 F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). @@ -969,6 +978,7 @@ ack short-horizon-churn 8a5a734986085360 5 S2c classifier-gap round: churn=self ack short-horizon-churn 8a92173ded649e17 39 cid=d49e411bf180293e by=src/* P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: 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 short-horizon-churn 8aa1aca1ece52982 3 R-I --quality-delta=A..B: printUsage + runQualityDelta grow by the new form's --help rows and its legend disclosure (both are contract surfaces, not incidental bulk); the two churn=self rows are this lane rewriting main.cpp/dmm.h bodies that recent commits also rewrote. Complexity was fixed by extracting resolveDeltaBasis, not acked. ack short-horizon-churn 8ac0e9a4081272f8 3 cid=285c05e869b57171 abstention round-2 lane: short-horizon churn on the ARB harness is the lane's own second edit to bench/arb this wave — run_arb.py had to learn the three new facts and sweep() had to write them; the churn is the round, not new debt in the tool +ack short-horizon-churn 8ad0db7b7210fd71 18 cid=eed9f2eeec92e17a E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn 8b4e4611159af966 13 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 8b78fc9166769dc2 5 cid=5543359c040daa6f at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack short-horizon-churn 8c36cf4c37be4542 8 cid=15b667b8b5141501 C4 recall budget spreading: recall.h's budget loop was rewritten load-allocate-emit, which necessarily edits committed lines the 2026-09-04 capture-audit round last touched. The churn is real and correctly measured; the edit IS the fix for the defect those lines carry (recallbudgetcheck 8.1/8.2/8.9). Nothing to refactor away. @@ -1015,8 +1025,9 @@ ack short-horizon-churn a75ae033caada1b3 10 cid=163ea9af8189c427 lane C plain-te ack short-horizon-churn a783c75feea9f253 44 W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d ack short-horizon-churn a7ec845d7950d0b5 6 cid=383e339bb184adff OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn a8b774025a21bdc6 83 cid=fa890c64b85771c2 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: M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. -ack short-horizon-churn a9f76a08efdb3d50 24 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. +ack short-horizon-churn a9f76a08efdb3d50 31 cid=fb7678445c3ca588 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. ack short-horizon-churn aa71fcdf69942431 66 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. +ack short-horizon-churn aaea365e24a94358 25 cid=a9ae44c1e9ef0617 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn ab7737f3582352d5 4 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ab9b3f1db516af63 21 cid=0b950315073c2901 by=src/* 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. | prior: A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn ac88b70b8c51cc70 14 L2 stale-ack disclosure: unavoidable growth/self-churn on runQualityDelta and qualityDeltaJson, the two pre-existing quality-delta dispatch hubs every new axis has to touch; logic already extracted to quality.h (staleForXxx/staleAcksXml/staleAcksJsonArray) to minimize the added footprint @@ -1038,6 +1049,7 @@ ack short-horizon-churn b792d6faac289d2e 154 cid=89cca6bb691a6095 C1 F-06/F-07/F ack short-horizon-churn b7da91c05624ab1b 6 cid=12ba71ea2a2cbf3d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn b7e9e25ba704623a 2 cid=7a87da721dc19be4 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. ack short-horizon-churn b8c5550b5e3dc150 6 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) +ack short-horizon-churn b8fb362316309156 13 cid=ced21816b0a38c38 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. ack short-horizon-churn b9a3c1188021d834 11 §L3: short-horizon-churn is an unavoidable byproduct of adding the grammars=/eligible_files=/of_files= disclosure to --match's emission point, which lives inside runLint — the function that already serves both --match and --lint. No way to add this output without editing it. ack short-horizon-churn ba2bfbd5e821ec8b 8 verifier F1/F2/F4/F5 round: short-horizon-churn=self on printUsage / formatRecallHeader / serialize / collectSources / emitRunLintSarif / kSkippedLegend / runLint / runSkipped is THIS change's own edit window and nothing else. Every finding of every other kind this round opened was FIXED rather than acked: emitLintSarif's complexity 11->20, verbosity 44->69, params 5->6 and api-surface 5->6 all went back to baseline by extracting writeSarifRuleDecl / writeSarifResult / writeSarifRunProperties and folding the run-level facts into one SarifRunProperties struct instead of a parameter per disclosure; emitRunLintSarif's params 8->12 went back to 8 by taking the MainDispatch every other handler already takes; formatRecallHeader's verbosity 55->67 went back by moving the rationale above the function. What is left is structural: a disclosure gap cannot be closed without editing the verb that has it. pruned_dirs= requires collectSources (the crawl branch that prunes) and runSkipped+kSkippedLegend (the verb and legend that report it) and printUsage (the flag's own help text); the SARIF selection/applicability crossover requires emitRunLintSarif and its runLint call site; the recall lines= clause requires formatRecallHeader. Same shape as the §L1 skip-taxonomy ack already in this file, which records exactly this set for exactly this reason. Gates: test/skipreasoncheck.sh arms 8+9 and test/sarifcheck.sh arms 8-10 both RED against a pristine HEAD binary and green after; pargates 415 green (412 pass, 3 skip, 0 fail); determinism + xmllint + SARIF JSON-validity all clean. ack short-horizon-churn baaadc036e16abed 39 cid=edac6f040440c34a recall CLI/MCP scorer unification: the one gating row is short-horizon-churn churn=self on mcpverbs.h::recallText (was=0 now=39), which is this lane's own edit to that symbol and not new debt - recallText SHRANK from a captureXml lambda over writeRecall to a single recallFor call, and the churn number counts the lines this commit moved through it. The two remaining new-symbol rows are recall.h::recallFor itself: api-surface new-symbol (it IS the new shared seam both front doors call, which is the point of the change) and params 0->8, the union of what buildRecall (8 params) and lexicalScores need to rank-then-build - no argument is new information, they are the arguments the two call sites already passed separately, moved to the one place neither door can now diverge from. The two ambient churn rows on serialize.h packBodies/packSignatures are comment-only edits repointing a stale writeRecall reference to buildRecall. CLI --recall output byte-identical to a pre-change binary on three corpora; 34 targeted gates green including the new test/recallparitycheck.sh (red on 8 of 12 arms pre-fix). @@ -1074,10 +1086,10 @@ ack short-horizon-churn ce968bdf8848c214 13 cid=3c5d6c07cbc4425e OPTREMARKS F3 ( ack short-horizon-churn d03215f48bec3886 3 cid=c7f3e74cf288dfc2 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper ack short-horizon-churn d05476877df0016e 6 cid=94feac15a58025e5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d3afca728392d688 6 cid=5765fc3aca7af273 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn d42c85b67bd0956f 28 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) +ack short-horizon-churn d42c85b67bd0956f 33 cid=30fecf6174abfa33 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn d44595768a7cf3af 2 cid=21539523996cfb13 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn d557a0077677ebd4 39 cid=7e6e8b041d54e368 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical -ack short-horizon-churn d63db6944aa504a7 43 cid=70fc957435aa6310 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 | prior: lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B +ack short-horizon-churn d63db6944aa504a7 44 cid=feca94020cbd1e3a Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: 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 short-horizon-churn d6e55b78e3a5fdda 7 cid=31161fe2ed1af5a5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d8aaf90801200a68 13 cid=d059a10443da1f80 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d9990dc7492a8bb2 20 cid=4646606f648e387e by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently @@ -1089,17 +1101,17 @@ ack short-horizon-churn dbe6ed5269d328a4 21 cid=9c4a7dc830d23192 M12 (capture-au ack short-horizon-churn dcb3ea81a3caeba3 7 cid=0d02d50c1afd1bec ingest-path disclosure: eval prints ingest: lex=rich|scan and --doctor prints rich_verbs= derived by asking needsValueUses (now ONE function in cli.h) per verb. short-horizon-churn on runEvalRetrieval is this session's repeated edits to it, not instability; mutation control in knownitemcheck proves both new arms fail when the eval verbs leave the predicate. ack short-horizon-churn dd02b378ae6b5b75 5 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack short-horizon-churn dd2f935e1818171a 4 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. -ack short-horizon-churn dd627540f10bba76 23 cid=e05706d6ee52ed9e lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: paper-shape lane (2026-08-28), --compress on served bodies (arXiv 2607.09691). api-surface/params on restatePackTaskBodiesWrapper: the +1 DEFAULTED bool is the compress disclosure thread — the wrapper REWRITES the open tag, so without it the restated bundle would silently claim uncompressed bodies (forcompresscheck arm 5); every other caller is byte-identical (default false). short-horizon-churn churn=self on packTaskBundleText/restatePackTaskBodiesWrapper is this lane's own edit window (the in.compress pass-through + the two hand-formatted wrapper tags). Red-first gate test/forcompresscheck.sh vs 1dc7b01; flagless byte-identity proven on 10 vectors incl. this repo's src; 607-vector argvdiff clean bar --help/--version/--compress-guard. 22 targeted gates green. +ack short-horizon-churn dd627540f10bba76 25 cid=f2adaaacf49b9a14 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked ack short-horizon-churn dda0db55532bd5e1 4 cid=8515eb8f5b5e8953 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: R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper ack short-horizon-churn df44625235cf2046 8 cid=a32d38683c291330 R8 printf-family pilot: std::printf/fprintf/snprintf -> std::print/std::format across every call site in verbs_lint.h, a file independently hot this round; converting a printf call necessarily rewrites the same recently-touched line, so self-thrash is inherent to the conversion, not new debt. Byte parity verified via test/printffmtparitycheck.sh (RED on an injected 1-byte mutation, GREEN on this pilot). ack short-horizon-churn dfc9aa3f1867944b 7 cid=4e20d921b9294272 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn dfdf15c302800e6f 39 cid=c3a68b23304447a1 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh ack short-horizon-churn dfe94683f484669b 12 T3 disclosure-gap fix 2026-08-22: verbosity/churn on the two emitters + harness trace persistence are the registered disclosure's own bytes and comments; the gate-helper clone follows the self-contained-MCP-gate convention (every mcp gate carries its own mcp_call) ack short-horizon-churn e0a00f3a5f72168d 22 cid=8df40da9967f24e3 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. -ack short-horizon-churn e14feca13c7ae680 10 cid=ae82b91f5a239bac cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. +ack short-horizon-churn e14feca13c7ae680 13 cid=3f37ac1bf2682450 Review of #214, round 2. The section now cuts over its own grouped, escaped rendering and the run-hint clause is gated on the row COUNT the emitter reports, so the seams that carry those two facts changed shape together and every gating row is that one change. api-surface writeFlipHeader 4->5: ONE parameter, the count of test files the caller already rendered, so the header can gate its clause on the rows the document will carry - a flip with paid 180 B for a rule about rows it has none of, and the header cannot see the corpus to count them itself (one caller, updated in the same commit). duplication/new-clone renderToString | chargeSection: chargeSection is the est_tokens family's FAULT-INJECTABLE buffer (openChargeBuffer is constexpr-false in release and is what estchargecheck degrade arm switches on), so it cannot route through a plain open_memstream seam without deleting the only reachable degrade path the gate has; mcpverbs captureXml WAS folded into the seam in this same commit rather than acked. short-horizon-churn churn=self on writeFlip/writeFlipHeader/writeHandoffPacket/writePrContext/partitionTestRows/dispatchMcpLine is this lane own footprint across the two rounds of one item - the E1 landing, the CI budget fix, the first review round and this one - none foreign, none thrash. | prior: E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself ack short-horizon-churn e17ff0a2142942c4 4 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn e1f67e3817c9c097 7 cid=ff9ddf76360520be OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn e3c54d39d366fbaa 7 cid=402f938df07371e3 M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. +ack short-horizon-churn e3c54d39d366fbaa 33 cid=c62e78b54758b435 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn e47e183ce409b3d1 5 cid=4a71f6160a322754 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: E3 (terminality round A, lane E): editpreviewcheck's fourth normalisation (strip the preview-only child), stated in the gate ack short-horizon-churn e514a69013d0934c 88 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites ack short-horizon-churn e602fc1bbc735406 5 cid=d2100a9e6dc3a52c OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -1125,7 +1137,8 @@ ack short-horizon-churn f3ea5dbe8b5720cb 6 cid=8b8d47057aed710b M10 (capture-aud ack short-horizon-churn f43e1f2748e92444 7 cid=0fba983986a21ff4 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn f4dcea0b719bea78 2 cid=8d14b541671382e9 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn f6dedd851e4b2a25 8 cid=282de1477b560a22 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked -ack short-horizon-churn f7cf3b3c2851b336 10 cid=d6c978525c223cb7 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: M1: three named helpers, each replacing an inline duplicate rather than adding surface. mcpVerbDeclaresLegend/mcpCompactLegendHint are the ONE place the compact-legend family and its schema keys are read (mcpCompactLegendHint is not new — it moved down from mcp.h, which includes mcpverbs.h, so the batch assembler can call it); applyCompactToBatchSubs is the shared rule both batch front doors call, CLI and MCP, so the posture cannot fork; appendPackTaskRootExtras splices the bundle's owed root attributes onto BOTH root spellings, which is exactly the bug it prevents. The two ambient-churn rows are the same edit measured by recency. +ack short-horizon-churn f78020a188f5b4c8 25 cid=705ca15ee956afa0 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself +ack short-horizon-churn f7cf3b3c2851b336 25 cid=697fe208251fea62 E1/A4-2: every tests_to_run emitter re-routed through testmap.h's one seam (testRowsRendered) so runner-less rows group by evidence; the 12 sites were rewritten in-window by M21(b) and F1, and touching that family once more is the item itself | prior: 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn f8d747d123b4143d 5 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn f99ee113e972ca80 10 cid=5d47fafedace31ba by=src/* M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. | prior: M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. ack short-horizon-churn fa6dd5532dd57a3c 3 cid=3104c098edd02883 A6: short-horizon churn on editplan::run, ::receipt and ::rollbackMessage is this fix round itself -- four assigned defects on one small surface, committed one per item, so the same three symbols fall inside the churn window more than once. churn=self, not instability in the code. diff --git a/CHANGELOG.md b/CHANGELOG.md index c82b31732..2588dbfa5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,192 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — tests-to-run rows without a runner are grouped by hop distance + +Every tests-to-run row that had no derivable runner said so on the row — `run_unknown="1"` in XML, +`"run_unknown":true` in JSON, `(run: not derivable)` in `--situ`'s text — and on a corpus where almost +no harness has a runner that was the same 16 or 23 bytes repeated once per row: on the RocksDB tree, +`--affected=db/write_batch.cc` listed 127 tests, 126 of them runner-less, and paid 2,016 B of XML and +2,898 B of text for one fact. Rows already come in evidence order (changed, partner, hops ascending, +path), so runner-less rows whose per-row attributes are byte-equal are now served as one row, +`` (JSON: `"p"` — or `"test"` — becomes an array beside +`"n"`; text: `[hops=2] (17): a, b, c (run: not derivable)`), emitted where its first member stood. Rows +with a runner stay single, a group of one stays a `` row, a path that contains a comma is never grouped +at all (`p=` is a comma-separated list and every XML parser undoes an entity before a consumer splits on the +delimiter, so an escaped comma would reappear as a separator and `n=` would disagree with what the reader +counts — the text twin had no escape to undo), and every path is kept verbatim — the multiset of paths +before and after is identical and so is their ORDER, which is what `test/testrowruncheck.sh` arm 12 proves +on a fixture with three hop groups and a runner row +in the middle of one of them, in all three dialects (red on the previous binary). All twelve emitters — +`--affected`, `--exercises`, `--test-gate` XML and JSON, `--situ`, `--pr-context`, `--handoff`, +`--flags --flip`, `--pack-task` XML and JSON, the MCP `situational_awareness` twin and the edit +receipt — render through one seam in `testmap.h`, and the M21(b) rule keeps its meaning: a `` or +`` row carries `run=` or `run_unknown="1"`, never neither. Measured on RocksDB (`wc -c`, same cache, +same commit): `--affected=db/write_batch.cc` 10,668 → 6,992 B, `--test-gate=db/write_batch.cc` 13,242 → +9,747 B (its JSON 11,055 → 7,163 B), `--situ=db/write_batch.cc` 11,769 → 7,357 B, and in the compact +dialect 9,312 → 5,313 B and 11,223 → 7,596 B; 8 `` rows replace 124 single rows (a group covers a +contiguous run only, so the one runner row inside the hops=2 tier splits it in two — order is preserved by +construction, `test/testrowruncheck.sh` arm 12 reads the paths back in emitted order) and the residual +spent on the disclosure is 160 B (XML, 10 `run_unknown="1"`) and 230 B (text) per list. +`--pack-task`'s tests section is byte-budgeted, so it CUTS over its own grouped, escaped rendering: it takes +the largest prefix of the row list whose rendered `` body fits the section budget, found by bisection +(the rendered size is monotone in the prefix length, so the bisection is exact), and counts `shown=`/`total=` +in test files. Measured on RocksDB with `--pack-task="change WriteBatch::Put"` at the default 6,000-token +budget, `wc -c`, same cache, same commit: `` where the pre-E1 bundle named 28, +the whole bundle 11,993 → 12,490 B. On this tree every harness has a runner, so nothing groups and the only +change is the legend that now defines ``: the `--test-gate` legend pin moves 2,720 → 3,000 B (measured +2,957) and the `ripwire.pack-task/v1` compact pin 820 → 880 B (measured 865), both because the compact +dialect and every rows-bearing full legend now define `run_unknown=` and `` — a definition +`--affected` and the compact dialect never carried. That compact `` term now says what the full clause +says, in the full clause's own words: its first form promised "every path verbatim (`,` a comma)", an +escape `testmap.h` does not emit — a path holding `,` is not grouped at all — and it never carried the rule +that a `shown=`/`total=` over these rows counts test FILES, so a reader holding only the compact legend was +told to undo an entity that is not there and disagreed with the full legend about what the pair counts. The +term goes 99 → 194 B and is charged only on a document that carries a `` row (measured on a fixture of six +runner-less tests, `--affected --legend=compact` 501 → 596 B); no pin moves, on this tree or on any gate +fixture, because every harness here has a runner and nothing groups. The two wordings cannot be one constant +— the compact dialect exists to re-spell, not to quote — so `test/compactlegendcheck.sh` arm (R) pins them +against each other, reading the phrases it requires out of `kRunHintLegendClause` itself rather than +restating them, and fails the next release where either wording drops one or promises `,` again (red on +the parent commit's source). The MCP manifest ceiling moves 42,384 → 42,800 B +(measured 42,777) for one 207-byte clause, plus the one-space separator that joins it to the sentence before +it, spliced into each of the two tool descriptions that serve these rows as JSON (2 × 208 B): +`situational_awareness` and `explore` return bare JSON with no legend of any kind, so a caller that +reads `p` as a string has nowhere else to learn that it can be an array. +The clause is rows-gated everywhere it is spliced — `--affected`, `--exercises`, `--pack-task`, the +partitioned bundle, and `--pr-context`, whose legend precedes its files in the STREAM but is now decided +after them: the chosen body is rendered first, the pricer charges the clause per candidate trim level from +that level's own body, and the form the chosen body was priced with is the form written, so the priced +legend and the delivered legend cannot disagree. A corpus-level predicate over-approximated it — a test +file outside the selected range, or a trim level whose `testCap` is 0, bought the clause for a document +with no row — and both now pay nothing (measured on `test/defaultceilingcheck.sh`'s 120-file, no-test +fixture: unconditional, the default bundle went 7,989 → 8,025 tokens over its 8,000 budget; gated, 7,989; +`test/prcontextcheck.sh` pins all four sides, red first). The clause is gated on a COUNT the emitter +reports with its rows, never on a search of the rendered bytes: `--pr-context` charges the trim level's own +row count and the partitioned bundle sums what each slice kept. Asking the bytes was wrong twice — a +`--pr-context` body and a `--pack-task` slice can both carry the literal text of the element inside CDATA, +and `--pack-task="write_report" --partition=2` over a two-file corpus with no test at all bought the outer +clause because one body prints `` (`test/testrowruncheck.sh` arm 15, red first). `--handoff` +and `--flags --flip` spliced the clause unconditionally and now ask the same count; `--handoff` is +byte-budgeted with heuristic rows dropped tail-first, so on a packet with no test row the 180 B it was +paying could evict a real row (arm 14, red first). + +Two byte-accounting rules changed with it. `--pack-task`'s tests section used to group FIRST and cut the +group rows with the generic list cutter under a per-row cap whose estimate was computed on UNESCAPED path +bytes, so a corpus whose test paths hold `&` or `<` rendered wider than the cap admitted; the cutter breaks +at the first over-budget entry, so the whole tail of the section went with it — `run=` singles included. +Measured on a matched pair of ten-test fixtures differing in one byte per name (`&` against `_`) at +`--token-budget=1440`: the control named 5 files and the `&` fixture named none. Cutting over the grouped, +escaped rendering fixes it and is strictly better than cutting the single rows and grouping afterwards, +which would have been safe but spends fewer of its bytes (2 files where grouping-first served 5); across +budgets 1440–1860 the new cut names 6–11 files against the old 5–11, and the `&` fixture never empties +(`test/testrowruncheck.sh` arm 13). And `--pr-context`, which must render a level to price it, rendered +through a helper that returned an empty string on an `open_memstream` failure with no alert at all — a +document could have shipped its legend, root and closing tag around an empty body claiming +`truncated="none"`. Every such render now goes through one seam in `infra/emit.h` (`rw::renderToString`, +which `packtask.h` already had in its own spelling) that reports the failure, and both `--pr-context` exits +fall back to streaming the level straight out: complete, correct bytes, a modelled estimate, and a +`DEGRADED_PATH_ALERT` saying which — serialize.h's own degrade contract. + +Six gates read the PATHS out of these rows, and each had its own reader: since a row can now name several +files, `grep -oE '"tests_to_run":\[[^]]*\]'` stopped at the first `]` (the end of the first group's path +array, so three arms asserted over two and a half rows and passed vacuously), `sed`-based XML readers saw +only the single rows, and the text reader took `$1` of a line that on a group line is `[hops=1]`. They all +want the same thing — the files named, in emitted order — so `test/affectedcheck.sh`, +`test/impactpartitioncheck.sh`, `test/receiptpostcheck.sh`, `test/rootrelemitcheck.sh`, +`test/selectorchaincheck.sh` and `test/testrowruncheck.sh` now all ask `test/testrowpaths.py`, one reader for +all three dialects and both row shapes. Two more gates read these rows and keep their own readers, because +neither asks for the paths: `test/listingpagingcheck.sh` sums `n=` over the group rows to prove the family +never pages, and `test/w3fixlegendcheck.sh` counts path occurrences on a `--situ` line. That shared reader +had two silences of its own, and both now fail loudly with a control in `test/testrowruncheck.sh` arm 16. Its +JSON slicer returned the same nothing for a document with no `tests_to_run` field and for one whose array +never closes, and the path reader turned that into an empty list at exit 0 — so a TRUNCATED document +asserted over zero rows and passed, which is the defect the file was written to end. The two are different +claims: no field is an answer (0 paths, exit 0), an unclosed list is exit 2 with a named reason. And the text +dialect's single-row reader took `(\S+)`, which stops at the first space, so a test path holding one was +reported truncated — a path that does not exist, produced silently. It now cuts the run suffix and the +renderer's own attribute tail (`[changed] [partner] [hops=N]`, in that order and no other) and keeps +everything between verbatim; what the text dialect still cannot resolve is a path holding the literal +three-space `(run: ` opener, because that dialect carries no escaping at all — XML and JSON are exact. + +Three more things the row work left half-said. `rw::renderToString` asked `open_memstream` and then ignored +what `fflush` and `fclose` answered, returning `ok=true` regardless: a memstream grows by `realloc`, so an +allocation failure the per-row writes swallowed surfaces at the flush, and it is the close that publishes the +buffer and its size at all. Reading them anyway is how a SHORT document passes for a whole one — the same +defect as the empty body one size smaller. Both results are now checked, the alert fires, and `--pr-context` +takes the streaming fallback it already documents. The MCP row-shape clause named the key `p`, and only one +of its three producers spells it that way: `situational_awareness` emits `test`, `explore` and the edit +receipt emit `p`. A clause naming the wrong key is worse than no clause, because a caller reads it as a +contract, so it names both per producer while the rules they share are still stated once; the manifest +ceiling moves 42,800 → 43,000 B for a measured 42,973 (the clause 207 → 305 B in each of the same two +descriptions, 2 × 98 B). And `renderToString` called the emitter outside any handler: a throw from it — +`std::bad_alloc` out of the `std::format` fallback is the reachable one, since the point of the seam is to +buffer a document whose size is not known in advance — skipped the `fclose`, the `free`, the alert and the +documented empty-result fallback in one jump, leaking the memstream and its buffer and handing the caller an +exception where its contract says `ok == false`. Measured on this tree with the fault injected: +`--pr-context` aborted at `rc=134` with **zero bytes** on stdout and `libc++abi: terminating due to uncaught +exception of type std::bad_alloc` — the whole document lost, not just its estimate. The seam now catches at +its own boundary, releases what it owns once, discloses, and returns the degraded value its callers already +read, so the same run exits 0 with a complete 14,627-byte well-formed document carrying the same 20 `` +rows as the undegraded control. The alert names the throw rather than borrowing the buffer's message, which +on that path would be a wrong cause attached to a right consequence. Because a throw path is otherwise +unreachable from a gate, it is driven by an in-source fault switch in `serialize.h`'s +`isChargeBufferFaultInjected` shape — non-NDEBUG only, read once per process, exact `"1"` the only ON value — +and `test/prcontextcheck.sh` arm (F) asserts the whole contract with its own observability probe, red on the +parent commit (`rc=134`, 0 B, no alert). That switch carries the `INFRA_` prefix rather than this project's: +everything under `src/infra/` is built to travel to another repository, and `test/infraportcheck.sh` (C) +refuses a layer file that names the host — it caught the switch's first spelling, which is the gate doing +exactly what it exists for. + +### Fixed — an unmeasured `est_tokens` said nothing, a no-throw contract threw, and two test-row readers still went quiet + +Six defects from one review, each of them a surface that was silently wrong rather than loudly broken. +**`--pr-context` shipped a wrong `est_tokens` with no disclosure.** When a trim level's measurement render +fails, `prRenderLevel` returns an EMPTY body; the ladder priced that empty body, the price fit, and the root +printed it — while `writePrContext` correctly streamed the complete untrimmed floor. The only signal was +`DEGRADED_PATH_ALERT`, which `src/infra/Diagnostics.h` compiles to `do {} while (0)` under `NDEBUG`, so the +binary a user installs printed a modelled number with nothing at all saying so (non-negotiable #3). The bytes +were never the bug and are unchanged — a failed measurement may not decide what the answer contains — so the +fact goes where this class of fact already lives: `truncated=` now carries `;est-unmeasured`, re-priced with +the label in place, and the legend defines it in the same voice as `budget-floor-exceeded`. That label is 15 +bytes and can ride beside `budget-floor-exceeded`, which takes `prBudgetTail`'s worst case from 248 B to +263 B: `tail[256]` (SEVEN bytes of margin, as `test/fixedbufsweep.sh` had warned in terms) becomes +`tail[320]`, 56 B of margin, and the sweep's row moves with the measured recomputation. `rw::formatTo` was +not what had been saving it — it truncates silently and its return is not read there, so an overrun would +have dropped the closing quote of `truncated="` and shipped a malformed root with no diagnostic. +**`renderToString`'s no-throw contract had a throwing last statement**: `out.text.assign( buf, sz )` is the +one allocation on the success path and sat outside the handler, so a `std::bad_alloc` from it escaped a +function documented to return `ok == false`, and jumped the `std::free( buf )` two lines below on the way +out — leaking the memstream buffer. It is caught in its own handler (the two failures need different +cleanup: the emitter's throw owns an open stream, this one owns only the buffer) with its own alert literal, +and control falls through to the single `free()`, so the buffer is released exactly once on every path. +Proved by `INFRA_FAULT_RENDER_COPY_THROW`, the twin of the emitter switch, in `test/prcontextcheck.sh` arm +(G) — red on the parent commit, and honest in both flavours: the switch and the alert live only on the +non-`NDEBUG` build, so the plain-flavour leg proves the degrade and the `NDEBUG` leg asserts only that the +verb is intact and that no false disclosure appears. The `est-unmeasured` LEGEND definition is asserted on +every flavour, which is the point of moving the disclosure off the alert. **The shared test-row reader's +malformed-field detector had a hole of its own species**: `test/testrowpaths.py` found `"tests_to_run"` and +then scanned arbitrarily far forward for a `[`, so `{"tests_to_run":null,"other":[{"p":"ghost.cpp"}]}` +sliced the NEXT field's array and returned `ghost.cpp` at exit 0 — a foreign field's paths served as this +field's answer, where the docstring already promised a `TestRowParseError`. The value is now read +adjacently (past the key, a `:`, optional whitespace, then `[` or raise); `null`, a number, a string and an +object all take the raise, in both `paths` and `jsonlist`, with a well-formed array and JSON whitespace as +controls (`test/testrowruncheck.sh` arm 17). **And two path readers had never been converted.** A census of +`test/` over the four shapes the reader was written to replace found `test/affectedcheck.sh`'s `tset()` — +in the file the reader's own docstring names among those it converted, so that claim was false — splitting +EVERY row's `p=` on `,` including a single row's, which turns a comma-bearing path (never grouped, by +`testmap.h`'s refusal) into two names that name nothing; and `test/testgatecheck.sh`'s `tset()` matching +` - + … @@ -1284,11 +1284,19 @@ $ ripwire . --test-gate # exit code: 4 ``` A `run=` attribute appears only when a runner is derivable from real evidence — a test-dir script -whose stem matches the harness, or whose text names it. No `run=` means *not derivable*, never a -guessed suite command. `script_gates_unmodelled="332"` is the same discipline: script-to-binary is not -a call edge, so those gates are invisible to this walk, and the number says so rather than letting -`tests="2"` read as complete. The `` rows are the untested blast radius: impacted symbols that no -test in the corpus reaches. +whose stem matches the harness, or whose text names it. A row with none says so — `run_unknown="1"`, +never a guessed suite command — and a `` or `` row carries one or the other, never neither. A +`` row is **two or more contiguous runner-less rows whose +attributes are byte-identical**, served as one: `n=` is how many, `p=` is their paths verbatim in list +order, and the disclosure is paid once per group rather than once per row. Everything else stays its own +row — a row with a `run=`, a row whose attributes differ from its neighbour's, and a path containing a +comma, which is never grouped at all, so `p=` splits on `,` into exactly `n=` paths. A `shown=`/`total=` +over these rows counts test FILES: a `` row is `n=` of them. + +`script_gates_unmodelled="332"` is the same discipline: script-to-binary is not a call edge, so those +gates are invisible to this walk, and the number says so rather than letting `tests="2"` read as +complete. The `` rows are the untested blast radius: impacted symbols that no test in the corpus +reaches. diff --git a/bench/arb/run_arb.py b/bench/arb/run_arb.py index e6fcfd286..da13ccb76 100644 --- a/bench/arb/run_arb.py +++ b/bench/arb/run_arb.py @@ -315,11 +315,40 @@ def trace_files(bin_path, snap, trace_text): TEST_RE = re.compile(r' row, so a group's p= is +# split on ','. THE CONTRACT, as testmap.h states it: a path containing ',' is NEVER grouped — it is served +# as a single row — so splitting a group's p= cannot split a path in half, and there is no comma escape to +# undo. This adapter used to undo a `,` that the seam stopped spelling on 2026-09-13 while decoding NONE +# of the entities it does emit, so `src/a&b.cpp` was scored against a file name that does not exist. +# Both row shapes now go through one decode, and therefore have one path contract. +GROUP_RE = re.compile(r']*?\bp="([^"]+)"') + +# The five XML attribute entities plus numeric references — test/testrowpaths.py's xml_unescape, in a file +# that may not import from test/. Local and small on purpose: an XML parser is not a dependency this bench +# adapter is allowed to grow for five substitutions. +_ARB_ENT = {"amp": "&", "lt": "<", "gt": ">", "quot": '"', "apos": "'"} + + +def xml_unescape(s): + def one(m): + body = m.group(1) + if body.startswith("#"): + try: + return chr(int(body[2:], 16) if body[1:2].lower() == "x" else int(body[1:], 10)) + except ValueError: + return m.group(0) + return _ARB_ENT.get(body, m.group(0)) + return re.sub(r"&([#0-9A-Za-z]+);", one, s) def affected_tests(bin_path, snap, changed_file): out = run_bin_or_none(bin_path, snap, ["--affected=%s" % changed_file]) - return [norm_path(m, snap) for m in TEST_RE.findall(out)] if out is not None else [] + if out is None: + return [] + paths = [xml_unescape(p) for p in TEST_RE.findall(out)] + for grp in GROUP_RE.findall(out): + paths += [xml_unescape(p) for p in grp.split(",") if p != ""] + return [norm_path(m, snap) for m in paths] def impact_files(bin_path, snap, seeds): diff --git a/skills/ripwire-mcp/SKILL.md b/skills/ripwire-mcp/SKILL.md index b234b55aa..32efce3f0 100644 --- a/skills/ripwire-mcp/SKILL.md +++ b/skills/ripwire-mcp/SKILL.md @@ -96,7 +96,7 @@ enclosing-chain report) and `@FILE:LINE` in any SYM selector; contract gate: `te | `grep` (`pattern`) | `--grep=STR` | parallel literal scan + enclosing symbol + matched line | | `cochange` (`file`) | `--cochange=FILE` | the lockstep git partners of one file | | `memory_recall` (`task`, `top_k` + `budget_tokens` optional) | `--recall=TASK [--top-k=N] [--max-tokens=N]` | full bodies of the few relevant docs/memory notes, bounded by the SAME default 8000-token body ceiling as the CLI (the header discloses `max_tokens=` and every cut). `budget_tokens` raises the ceiling explicitly when you want everything; `top_k` (default 8) shapes how many docs | -| `situational_awareness` (`diff`/`files` optional) | `--situ` | blast radius, tests_to_run, forgotten co-change partners (the Shotgun Surgery check), hotspot alert — as JSON; defaults to `git diff HEAD` | +| `situational_awareness` (`diff`/`files` optional) | `--situ` | blast radius, tests_to_run, forgotten co-change partners (the Shotgun Surgery check), hotspot alert — as JSON; defaults to `git diff HEAD`. In `tests_to_run`, `situational_awareness` uses `test`; `explore` and edit receipts use `p`. The field is a path string OR an **array** of paths beside `n` — several runner-less tests sharing their attributes, served as one row — and every row carries `run` or `run_unknown: true` | | `mentions` (`symbol`) | `--mentions=SYM` | which markdown plans/designs discuss a symbol | | `owners` (`symbol` optional) | `--owners[=SYM]` | bus-factor: recency-weighted author ownership | | `lego` (`type`) | `--lego=TYPE` | an interface's method contract + every implementor (own-language) | diff --git a/src/compactlegend.h b/src/compactlegend.h index b8aaa213a..4f57b4348 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -285,6 +285,28 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // --uses=Owner.field's member form (fielduses.h appends kUsesFieldLegend to that answer alone). owner_candidates= is a // row attribute that exists only beside member=, so one head term defines the whole form. { "member", "member=Owner.field: rows use that field; pinned=/amb_sites= rows with one owner/with owner_candidates=K; owners_of_name= fields so named" }, + // M21(b) / E1 (2026-09-12): the tests_to_run family's not-derivable disclosure, and the group row it rides + // once per group. Row-level (every dialect puts it on the row), present-only; the reading is qualified + // to that element so a document of single rows never pays for it, and a --flags document's own never + // triggers it (that element carries no run_unknown=). + // + // THE COMPACT TERM SAYS WHAT THE FULL CLAUSE SAYS (review of 6621370f). This row is the compact dialect's + // ONLY reading of , so a reader holding it and nothing else must be able to act on p=. It promised + // "every path verbatim (, a comma)" — an escape testmap.h no longer emits, because a path holding ',' + // is not grouped at all now — and it never carried the counts-FILES rule the full clause gained with it. + // So on a comma-path corpus `--affected --legend=compact` told its reader to undo an entity that is not + // there, and disagreed with the full legend about what a section's shown=/total= counts. + // The two facts a consumer cannot act without are now stated here in the FULL CLAUSE'S OWN WORDS: a + // path holding ',' is never grouped, so p= splits into exactly n=; and a shown=/total= over these rows + // counts test FILES. The compact dialect re-spells rather than quotes — that is what makes it compact, and + // kRunHintLegendClause is 350+ B against this table's per-verb charge — so the two cannot be ONE constant. + // test/compactlegendcheck.sh (R) pins them against each other instead: it reads the required phrases OUT + // OF kRunHintLegendClause and fails the next release where either wording drops one or promises , + // again. Cost of saying it: the term goes 99 -> 194 B, charged ONLY on a document that carries a row — + // measured on a six-runner-less-test fixture, `--affected --legend=compact` 501 -> 596 B, and no pinned + // legend on this tree or on any gate fixture moves at all, because every harness here has a runner. + { "run_unknown", "run_unknown=1: no runner derivable (a guess would be worse)", true }, + { "run_unknown", ": n runner-less rows with equal attrs as ONE row, paths verbatim; a path holding ',' is never grouped, so p= splits into exactly n=; shown=/total= over these rows counts test FILES", true, "g" }, { "hits_capped", "hits_capped=1: hits= is a floor" }, // Both also ride the map header: est_tokens= alone there under order=stable (the root drops it), over_ceiling=1 there // under max-tokens. Same number, same reading, so one row reads both places. diff --git a/src/flipimpact.h b/src/flipimpact.h index ce81f346f..f89654858 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -1140,8 +1140,11 @@ inline constexpr const char* kFlipRowLegend = // The doc comment, the `` header attributes, and the four situational rows that qualify them // (already-lit / also / parent / capped) plus the family roll-up. +// `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. inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex, - const std::string& nextInvocation = std::string() ) + const std::string& nextInvocation, std::size_t testFilesRendered ) { rw::emitTo( out, "", - // M21(b): the run=/run_unknown= rule, from testmap.h's ONE constant. - std::string( rw::kRunHintLegendClause ).c_str(), kFlipRowLegend ); + // 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::emitTo( out, " 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::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 ); writeFlipLights( out, res, ing, ex, maxRows, pageOffset ); for( const ValueBinding& b : res.bindings ) @@ -1261,11 +1272,12 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult // with 26 reachable tests named 25 of them and dropped the 26th because it sorted last. --test-gate's // own listing has never been windowed for exactly this reason; this listing is the same obligation // read from a different seed, so it is served whole on every page. SIZE_MAX, not maxRows. - const rw::TestRunnerIndex flipRunners( ing ); - writeCappedList( out, "tests", res.tests, SIZE_MAX, [ & ]( std::uint32_t f ) - { - rw::emitTo( out, "", ex( rel( f ) ).c_str(), rw::runAttrDisclosed( flipRunners, f, ex ).c_str() ); - } ); + // E1: rows without a runner are grouped (testmap.h's seam), so the listing is rendered whole and wrapped here + // exactly as writeCappedList wraps an uncut list — `` with no cut attributes, n= the FILE count, + // which is the same number the header's clause was gated on (flipTests.files, rendered above). + rw::emitTo( out, "", res.tests.size() ); + rw::emitRaw( out, flipTests.text.c_str() ); + rw::emitRaw( out, "" ); writeCappedList( out, "untested", res.untested, maxRows, [ & ]( NodeId u ) { const Symbol& s = ing.symbols[u]; diff --git a/src/handoff.h b/src/handoff.h index 4ed47930c..77e9dc47e 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -279,14 +279,12 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In // TestRunnerIndex is lazy: a packet with no test row reads no runner script. const rw::TestRunnerIndex hoRunners( ing ); const auto hoEsc = [ & ]( std::string_view t ) { return std::string( escapeXml( t, esc ) ); }; - for( const std::uint32_t f : facts.tests ) - { - v += " 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 + // BYTE-BUDGETED with heuristic rows dropped tail-first, so a packet with could evict a real + // row to pay 180 B for a rule about rows it has none of. + const rw::JoinedTestRows hoTests = rw::testRowsList( hoRunners, rw::testRowsOutOf( facts.tests, hoPathRel ), rw::TestRowShape{ rw::RowDialect::Xml, "t" }, hoEsc ); + v += hoTests.text; v += ""; // ── heuristic rows, priority order (dropped TAIL-FIRST under a budget) ─────────────────────────── @@ -388,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::kRunHintLegendClause; // M21(b): the ONE wording, spliced — never a seventh paraphrase + doc += rw::runHintClauseIfRows( hoTests.files ); // 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 += " +#include +#include // std::bad_alloc — renderToString's injected emitter-throw fault, below #include #include #include #include +#include #include #include #include @@ -151,4 +156,157 @@ template inline std::size_t formatTo( char* buf, std::size_t cap, st return static_cast( r.size ); } +// ── THE render-an-emitter-into-a-string seam ───────────────────────────────────────────────────────── +// An emitter writes to a FILE*. A caller that must MEASURE what it wrote (a budget ladder pricing its own +// document before it commits to a trim level) or REORDER it (a legend whose wording depends on the body that +// follows it in the stream) needs those bytes as a string first. That is one seven-line memstream dance, and +// it was hand-written at each such site. +// +// Review of #214: the copy in prcontext.h returned "" on failure with NO alert, and the unbudgeted +// --pr-context path had just been routed through it — so an open_memstream failure would have shipped a +// legend, a root tag and a closing tag around an EMPTY body, with truncated="none" saying nothing was cut. +// A degrade has to be visible and the caller has to be able to see it: `ok` is false exactly when the bytes +// returned are not the bytes the emitter wrote (and then `text` is empty), the alert names the site through +// the caller's own message — "which buffer failed" is the useful half — and the caller then takes its own +// documented path. Never a silent empty body, and never a silent SHORT one. +// +// CodeRabbit on #214: the first version asked open_memstream and then ignored what fflush and fclose +// answered, setting ok=true regardless. Both can fail, and either failure means the same thing: `buf`/`sz` +// are not the whole document. A memstream grows by realloc, so an allocation failure the per-row fwrites +// swallowed surfaces at the FLUSH; and it is fclose's final flush that publishes *buf and *sz at all, so a +// failure there leaves them stale or unset. Reading them anyway is exactly how a TRUNCATED document passes +// for a whole one — the same defect as the empty body above, one size smaller and harder to see. Both +// results are checked; fclose still runs whatever fflush said, because the stream has to be closed either +// way, and it runs exactly once. `buf` is freed once, on every path (free( nullptr ) is a no-op). +struct Rendered +{ + std::string text; + bool ok = false; +}; + +// ── THE EMITTER ITSELF MAY THROW, and that is a degrade, not a way out ─────────────────────────────── +// The emitter was called outside any handler. A throw from it — std::bad_alloc out of the std::format +// fallback is the reachable one, since the whole point of this seam is to buffer a document whose size is +// not known in advance — skipped the fclose, the free, the alert and the documented empty-result fallback in +// one jump: the memstream and its buffer leaked, and the caller got an exception where its contract says it +// gets `ok == false`. +// +// The answer is the one this layer gives everywhere a throw crosses a seam that owns a resource: catch AT +// the seam, release what it owns, DISCLOSE, and hand back the degraded value the caller already reads. It is +// the same answer this function gives when open_memstream fails, so callers need no new case — `ok == false` +// has always meant "these are not the bytes the emitter wrote", and a caller that has a fallback for the +// empty buffer has one for this. +// +// FAULT INJECTION, because a throw path is otherwise unreachable from a test: non-NDEBUG only (so it is +// `constexpr false` and the getenv is deleted in release: zero release cost), read ONCE per process (so it +// cannot change mid-document and determinism holds), and EXACT "1" is the only ON value, because the +// contract is a switch — a prefix test would let "=10" and "=0" mean whatever the reader guessed, which is a +// defect worth fixing once, here, rather than once per switch. +// +// THE ENV NAME CARRIES THIS LAYER'S PREFIX, NOT THE HOST'S. Everything under infra/ is built to travel to +// another repository; a host-named switch would arrive there describing a program its reader has never run, +// which is exactly what the layering gate refuses. A host that owns its own fault switches names them its +// own way and reads them through faultSwitchOn below. +#ifndef NDEBUG +inline bool faultSwitchOn( const char* envName ) noexcept +{ + const char* value = std::getenv( envName ); + return value != nullptr && std::strcmp( value, "1" ) == 0; +} +#else +inline constexpr bool faultSwitchOn( const char* ) noexcept { return false; } +#endif + +// Each switch keeps its own named wrapper and its own once-per-process read: the name is what a reader greps +// for, and the `static` is what keeps the answer from changing mid-document (determinism). +inline bool isRenderEmitThrowFaultInjected() noexcept +{ + static const bool isOn = faultSwitchOn( "INFRA_FAULT_RENDER_EMIT_THROW" ); + return isOn; +} + +// The SECOND throwing site, and its own switch for the same reason the first has one: the name is what a +// reader greps for. The emitter is not the only allocation here — the final copy out of the memstream buffer +// is one too, and it sat outside the handler that the emitter's throw takes. +inline bool isRenderCopyThrowFaultInjected() noexcept +{ + static const bool isOn = faultSwitchOn( "INFRA_FAULT_RENDER_COPY_THROW" ); + return isOn; +} + +template +inline Rendered renderToString( Emit&& emit, const char* degradeMsg ) +{ + Rendered out; + char* buf = nullptr; + std::size_t sz = 0; + std::FILE* m = open_memstream( &buf, &sz ); + if( !m ) + { + DEGRADED_PATH_ALERT( degradeMsg ); + return out; + } + try + { + // The injected fault stands exactly where a real std::bad_alloc would escape: the stream is open and + // nothing has been cleaned up yet, which is the state the catch below exists to unwind. + if( isRenderEmitThrowFaultInjected() ) { throw std::bad_alloc(); } + emit( m ); + } + catch( ... ) + { + // Everything this function owns, released once, in the order the non-throwing path releases it. The + // stream is closed rather than flushed first: there is no document to salvage, and fclose frees the + // FILE either way. `out` is still the default-constructed failure — empty text, ok == false. + std::fclose( m ); + std::free( buf ); + // NOT degradeMsg: that one says the BUFFER failed, and here it did not — the emitter did. The macro + // takes a const char*, so this is its own literal rather than a composed string; the caller is named + // anyway, because __PRETTY_FUNCTION__ carries the Emit lambda's own file and line. + DEGRADED_PATH_ALERT( "renderToString: the emitter THREW — nothing was measured, " + "the caller takes its documented fallback" ); + return out; + } + // Order matters: fflush first (it reports the write error), then fclose UNCONDITIONALLY (it owns the + // stream, and skipping it on a flush failure would leak it). A null buf after a clean close is itself a + // failure — an emitter that wrote nothing still gets a zero-length, null-terminated buffer. + const bool flushed = std::fflush( m ) == 0; + const bool closed = std::fclose( m ) == 0; + out.ok = flushed && closed && buf != nullptr; + if( out.ok ) + { + // THE LAST ALLOCATION IS STILL AN ALLOCATION. This copy is the one throwing statement on the success + // path, and it used to stand outside every handler: a std::bad_alloc here escaped a function whose + // contract is that a failure is ALERTED and returned as ok == false, and it jumped the free() below + // on the way out, leaking the memstream buffer. Caught here rather than in one handler around the + // whole body, because the two failures need different cleanup: the emitter's throw owns an OPEN + // stream (fclose + free, in the catch above), while by this point the stream is already closed and + // only `buf` is left — and control falls THROUGH to the single free() below, so buf is released + // exactly once on every path, success and failure alike. + try + { + // The injected fault stands exactly where a real std::bad_alloc would: the buffer is complete + // and closed, and the copy of it is what fails. + if( isRenderCopyThrowFaultInjected() ) { throw std::bad_alloc(); } + out.text.assign( buf, sz ); + } + catch( ... ) + { + out.ok = false; + out.text.clear(); + // NOT degradeMsg, and not the emitter's literal either: the buffer did not fail and the emitter + // did not throw — the copy out of a complete buffer did. Same reasoning as the catch above, so + // the caller reads which of the three failures it actually hit. + DEGRADED_PATH_ALERT( "renderToString: the final COPY out of the buffer THREW — nothing was " + "measured, the caller takes its documented fallback" ); + } + } + else + { + DEGRADED_PATH_ALERT( degradeMsg ); + } + std::free( buf ); + return out; +} + } // namespace rw diff --git a/src/mcp.h b/src/mcp.h index 2ce96f295..c5c867bde 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -129,6 +129,26 @@ inline constexpr std::string_view kAtSeedShortClause = "@FILE:LINE line-seeds re // NAME, so a line-seed cannot narrow the scan — instead it REBINDS to the innermost enclosing definition // and the answer says so ('sym'), the one-step-smart-defaults posture: the call carries the answer, never // a pass-the-name-yourself retry. +// E1 / review of #214: the tests_to_run ROW SHAPE, for the three tools that serve those rows as JSON. The +// rows grew a second shape and these descriptions still promised the first — a caller reading `p` as a +// string breaks on the array, and `run_unknown` appeared nowhere in this file at all. ONE wording, spliced, +// never a fourth paraphrase; the manifest ceiling moves with it, measured, in the same commit. +// +// CodeRabbit on #214: the first wording named the key `p`, and only one of the three producers spells it +// that way. situational_awareness emits "test" (mcpverbs.h, TestRowShape{ Json, "test" }); explore +// (packtask.h) and the edit receipt (mcpedit.h) emit "p". So the clause told a situational_awareness caller +// to read a key its answer does not carry — worse than the silence it replaced, because it reads as a +// contract. Both spellings are named, per producer, in the SINGLE quotes kAtSeedRebindClause already uses +// for a key: this string is spliced straight into the tools/list JSON, so a double quote here has to survive +// a C++ literal AND JSON escaping to keep the manifest parseable — the first draft did not, and mcpmanifest- +// check caught it as a JSONDecodeError rather than a byte count. The key is the ONLY thing that differs: the +// string-or-array rule, the `n` beside an array, and the run/run_unknown obligation are one rule for all +// three, and are stated once. +inline constexpr std::string_view kTestRowJsonShapeClause = + "tests_to_run rows: the path key is 'test' on situational_awareness and 'p' on explore and the edit " + "receipts; its value is a path STRING, or an ARRAY of paths beside n when several runner-less tests share " + "their attributes and are served as ONE row; every row carries run (the command) or run_unknown:true. "; + inline constexpr std::string_view kAtSeedRebindClause = "An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, " "disclosing the rebound name as 'sym' (see find_symbol for the seed grammar)."; @@ -720,7 +740,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // while the CLI --recall began honoring --top-k in this round's Wave 1. "{\"name\":\"memory_recall\",\"description\":\"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = the body ceiling in tokens (default 8000) — it SHAPES to fit, the CLI --recall's --max-tokens, not --token-budget's refuse-if-over GATE, and the header discloses max_tokens= and every cut.\"," + mcprefuse::toolMetadataFor( "memory_recall", pathIsRequired ) + "}," - "{\"name\":\"situational_awareness\",\"description\":\"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEAD'. files is a STRING of comma-separated paths (files=\\\"src/a.cpp,src/b.h\\\"), not an array; an array is refused rather than read as absent, which would answer about the working tree instead of the files you named. limit/offset page blast_radius and forgotten only; with no limit every row is served, as always.\"," + "{\"name\":\"situational_awareness\",\"description\":\"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEAD'. files is a STRING of comma-separated paths (files=\\\"src/a.cpp,src/b.h\\\"), not an array; an array is refused rather than read as absent, which would answer about the working tree instead of the files you named. limit/offset page blast_radius and forgotten only; with no limit every row is served, as always. " + std::string( kTestRowJsonShapeClause ) + "\"," + mcprefuse::toolMetadataFor( "situational_awareness", pathIsRequired ) + "}," "{\"name\":\"mentions\",\"description\":\"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. " + std::string( kAtSeedRebindClause ) + "\"," + mcprefuse::toolMetadataFor( "mentions", pathIsRequired ) + "}," @@ -764,7 +784,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // L4 — the one-call orientation front door + B11 verb parity. `explore` is // the SAME handler as the CLI --pack-task; the older name `pack_task` still dispatches (tools/call // name=="pack_task" works) but is not separately advertised here — see mcp.h's kMcpVerbTable comment. - "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional (default 6000); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices.\"," + "{\"name\":\"explore\",\"description\":\"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gracefully and reports every truncation. Replaces the for -> fetch_body -> find_referencing_symbols -> memory_recall dance when you want the whole orientation at once; for JUST the ranked inventory use 'for'. Same handler as the CLI --pack-task. ALIAS: tools/call name='pack_task' answers this exact tool with these exact arguments (it gets no separate tools/list entry). task = the task in plain words; budget_tokens = optional (default 6000); partition = optional 2..16, refused outside that band — FANNING OUT to N agents on ONE task? Ask for it and get one shared core plus N minimally-overlapping slices carved along the call graph's own communities, budget_tokens then meaning ONE agent's budget. Read overlap_max/split before trusting the slices. " + std::string( kTestRowJsonShapeClause ) + "\"," + mcprefuse::toolMetadataFor( "explore", pathIsRequired ) + "}," "{\"name\":\"from_trace\",\"description\":\"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed and counted, never ranked, and the counters CLOSE (in_corpus = suspects + merged + unresolved). Each frame binds by its own NAME first, falling back to the def enclosing its line only when that name is absent or ambiguous — resolved_by= and any name-vs-line disagreement are disclosed, never silently rebound. Same handler as the CLI --from-trace. A failing-test trace also gets a test_hop block reaching the source symbols behind the assertion, labelled heuristic. trace = the raw trace TEXT (paste it, don't hand-translate it into a query); budget_tokens optional.\"," + mcprefuse::toolMetadataFor( "from_trace", pathIsRequired ) + "}," diff --git a/src/mcpedit.h b/src/mcpedit.h index 29ce9bb98..10e41610b 100644 --- a/src/mcpedit.h +++ b/src/mcpedit.h @@ -1035,10 +1035,11 @@ namespace mcpedit 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\":["; + std::vector rcRows; + rcRows.reserve( ans.rows.size() ); for( std::size_t i = 0; i < ans.rows.size(); ++i ) { TestRow row = ans.rows[i]; // by value: see below - if( i ) { out += ","; } // A matched TEST file's changed= is spelled seed_kind="test" on --affected (verbs_change.h does // exactly this), because "the argument matched it, run it" is a different fact from "you edited // a file this test reaches". The receipt stands in for that verb, so it spells it the same way. @@ -1048,11 +1049,10 @@ namespace mcpedit // use, so a receipt row can never say less than the verb it stands in for. A row that arrived on // partner= or seed_kind= alone is a WEAKER claim than a graph-reached one, and dropping the // attribute would serve it as though it were the same. - out += "{\"p\":\"" + mcpdetail::jsonEscape( std::string( rw::sarif::rootRelativeUri( ing.files[ row.fileId ], prefix ) ) ) + "\"" - + ( seedTest ? ",\"seed_kind\":\"test\"" : "" ) - + rw::testRowEvidence( row, rw::EvDialect::Json ) - + rw::runFieldJsonDisclosed( runners, row.fileId, jesc ) + "}"; + rcRows.push_back( { row.fileId, std::string( rw::sarif::rootRelativeUri( ing.files[ row.fileId ], prefix ) ), + std::string( seedTest ? ",\"seed_kind\":\"test\"" : "" ) + rw::testRowEvidence( row, rw::EvDialect::Json ) } ); } + out += rw::testRowsJoined( runners, rcRows, rw::TestRowShape{ rw::RowDialect::Json, "p" }, jesc, "," ); // E1: --affected's , "p" an array out += "]"; // the root-level companions --affected carries beside its rows, so the two documents disclose the // same facts about the same list diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 9630d0d18..c30c74289 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -368,23 +368,13 @@ inline std::string mcpUnknownFieldRefusal( const std::string& scope, std::string return {}; } -// Capture one FILE*-writing renderer into a string. The three verbs below differ only in which writer they -// run, so the open_memstream boilerplate lives here once instead of three times. +// Capture one FILE*-writing renderer into a string — infra/emit.h's ONE renderToString seam with this +// surface's own degrade wording. It kept its own copy of the memstream dance until the review of #214 +// gave the tree a single seam for it; the contract is unchanged (an allocation failure is an empty string, +// never a NULL deref), and it now also ALERTS, which this copy never did. inline std::string captureXml( const std::function& render ) { - char* buf = nullptr; - std::size_t sz = 0; - std::FILE* mem = open_memstream( &buf, &sz ); - if( !mem ) - { - return {}; // alloc failure → empty, never deref NULL - } - render( mem ); - std::fflush( mem ); - std::fclose( mem ); - std::string out = buf ? std::string( buf, sz ) : std::string{}; - std::free( buf ); - return out; + return rw::renderToString( render, "mcp: open_memstream failed — this verb answers empty" ).text; } // full pipeline on a dir → XML captured into a string (captureXml, above). @@ -1331,18 +1321,8 @@ inline std::string situationDiffJson( const std::string& root, const std::string out += "]"; out += graphCountFloorAttrJson( ix.g ); // H5/M15: blast_radius[].dependent_symbols is read off the name-based CSR — a floor, with the gauge out += ",\"tests_to_run\":["; - { - bool first = true; - for( std::uint32_t f : facts.tests ) - { - if( !first ) - { - out += ","; - } - first = false; - out += "{\"test\":\"" + mcpdetail::jsonEscape( std::string( situJPathRel( f ) ) ) + "\"" + runFieldJsonDisclosed( runners, f, jsonEsc ) + "}"; - } - } + // E1: grouped where no runner is derivable — "test" is then an ARRAY of paths (testmap.h's seam) + out += testRowsJoined( runners, testRowsOutOf( facts.tests, situJPathRel ), TestRowShape{ RowDialect::Json, "test" }, jsonEsc, "," ); // F3: the decl/def partners of the changed set — the header/impl relationship the blast_radius array // above cannot carry, because a header does not transitively depend on the source that implements it. diff --git a/src/packtask.h b/src/packtask.h index 9bde9bcfe..b4539b5f0 100644 --- a/src/packtask.h +++ b/src/packtask.h @@ -148,6 +148,10 @@ inline constexpr int kPackTaskQuotaTestsPct = 10; // the cascaded remainder static_assert( kPackTaskQuotaRankingPct + kPackTaskQuotaBodiesPct + kPackTaskQuotaCallersPct + kPackTaskQuotaNotesPct + kPackTaskQuotaTestsPct == 100, "pack-task section quotas must sum to 100%" ); +// E1 / review of #214: one entry, one unit again. The section is CUT over single rows — one row per +// test file, whose rendered bytes are exactly what the cut measures — and only the KEPT prefix is grouped +// afterwards (see the tests section below), so shown=/total= count rows and test files at the same time and +// the per-entry `units` arithmetic this struct carried for one release is gone with the estimate that needed it. struct PackTaskSection { std::string xml; std::size_t kept = 0; }; // W3FIX H2/M1 — the pieces the header comment is made of, so the header can be REBUILT in three shapes (as @@ -169,6 +173,8 @@ struct PackTaskHeaderParts std::string_view rootArg; // R-E (2026-08-17): the single-root run's own root= — the ladder's // route-dropped rebuild below calls ctxRootOpen a second time and // must carry the SAME root as the pre-built rootOpenStr did. + std::string_view runClause; // M21(b)/E1: testmap.h's run=/run_unknown=/ clause — ROWS-GATED (empty when + // the tests section kept no row), so a bundle that omits tests pays nothing }; // P10 (L7): the bundle legend's BODY, one constant, spliced by the standalone header (packTaskHeaderText) and @@ -280,6 +286,7 @@ inline std::string packTaskHeaderText( const PackTaskHeaderParts& p, bool withRo h.append( p.expandNote ); h.append( p.docMentionNote ); h += kPackTaskBundleLegendBody; // P10 (L7): the body is ONE constant — the partitioned document states it once for all slices + h.append( p.runClause ); // M21(b)/E1: the row's run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated h.append( p.report ); h.append( extraNotes ); h += " -->"; @@ -334,6 +341,88 @@ inline PackTaskSection packTaskListSection( std::string_view tag, std::string_vi return out; } +// ── THE SECTION — the one list whose entries are not independent ──────────────────────────────── +// Every other section here is a list of rows the budget can cut anywhere: entry i costs entry i's bytes, +// whatever its neighbours are. Test rows are not like that. E1 serves a run of runner-less rows with equal +// attributes as ONE row, so admitting one more FILE can cost a whole new row or just `,path` on the row +// already there — the cost of the k-th file depends on the k-1 before it, and packTaskListSection's +// "used += e.size()" has no way to say that. +// +// The first attempt grouped FIRST and handed the group rows to that helper under a per-row byte cap, with a +// cap estimate of `attrs + 48 + Σ( path + 1 )` computed on UNESCAPED paths. Review of #214: a corpus whose +// test paths hold '&' or '<' renders wider than the estimate admitted, the helper broke at the first +// over-budget entry, and the whole tail of the section went with it — `run=` singles included. Measured on a +// ten-test fixture at --token-budget=1440, `_`-named paths served 5 files and the same fixture with `&` in +// every name served NONE: the section vanished rather than shrank. +// +// So this section CUTS WHERE THE BYTES ARE. It asks the only question that matters — what is the largest +// PREFIX of the row list whose GROUPED, ESCAPED rendering fits the budget — and answers it by rendering +// candidate prefixes and measuring them. Cutting a prefix and grouping it afterwards would also have been +// safe (grouping only shrinks), but it cuts over the single rows' bytes and then spends fewer of them: on +// that same fixture it served 2 files where grouping-first served 5, which throws away the win E1 exists for. +// +// The search is a bisection, which is exact because `bodyOf` is monotone in k: extending the prefix by one +// row either appends a row or extends the last group by `,path` (a group of one is rendered as a single +// row, and the two-member that replaces it is strictly wider), so the rendered size never falls +// as k rises. ~log2(n) renders of an O(n) body, against a list that is a few hundred rows at most. +// +// shown=/total= therefore count test FILES — one qualifying file, one entry, on both sides of the cut — and +// a row carries N of them, which is exactly what the bundle legend now says. +template +inline PackTaskSection packTaskTestsSection( const rw::TestRunnerIndex& runners, std::span rows, + std::size_t budget, std::size_t wrapReserve, EscapeFn esc, + std::vector>* partitionOut ) +{ + PackTaskSection out; + if( partitionOut ) + { + partitionOut->clear(); + } + const auto bodyOf = [ & ]( std::size_t k, std::vector>* keepPart ) -> std::string + { + const std::span prefix( rows.data(), k ); + std::vector> part = rw::partitionTestRows( runners, prefix ); + std::string body; + for( const rw::RenderedTestRow& r : rw::testRowsRendered( runners, prefix, rw::TestRowShape{ rw::RowDialect::Xml, "test" }, esc, &part ) ) + { + body += r.text; + } + if( keepPart ) + { + *keepPart = std::move( part ); + } + return body; + }; + if( rows.empty() || budget <= wrapReserve ) + { + return out; + } + std::size_t lo = 0; // always fits (an empty body is wrapReserve alone) + std::size_t hi = rows.size(); // may not + while( lo < hi ) + { + const std::size_t mid = lo + ( hi - lo + 1 ) / 2; // lo < mid <= hi + if( wrapReserve + bodyOf( mid, nullptr ).size() <= budget ) + { + lo = mid; + } + else + { + hi = mid - 1; + } + } + out.kept = lo; + if( out.kept == 0 ) + { + return out; + } + const std::string body = bodyOf( out.kept, partitionOut ); + char open[ 160 ]; + rw::formatTo( open, sizeof( open ), "", out.kept, rows.size(), out.kept < rows.size() ? 1 : 0 ); + out.xml = std::string( open ) + body + ""; + return out; +} + // §H5, the direction the finding did not name: the XML names each over-budget skip in a // `` comment and the JSON dialect named none of them. Same set, same // reason, spelled for a parser. The key is OMITTED (not `[]`) when nothing was skipped, matching the @@ -747,18 +836,14 @@ inline std::vector buildMaskedRank( const IngestResult& ing, const std::v return masked; } -// a generic "render into a memstream, DEGRADED_PATH_ALERT + \"\" on failure" wrapper — shared by every -// packTaskBundleText section (and renderRankingWithFar below) so none of them hand-roll the memstream dance. +// every packTaskBundleText section (and renderRankingWithFar below) renders through infra/emit.h's ONE +// renderToString seam, with this file's own degrade wording. It keeps the name it had: a section that +// degrades is SKIPPED from the budget (empty string, the caller's documented path), so the `ok` flag the +// seam returns has no second reading here and the call sites stay one expression long. template inline std::string packTaskRenderToString( Emit&& emit ) { - char* buf = nullptr; std::size_t sz = 0; - std::FILE* m = open_memstream( &buf, &sz ); - if( !m ) { DEGRADED_PATH_ALERT( "pack-task: open_memstream failed — section skipped from the budget" ); return {}; } - emit( m ); - std::fflush( m ); std::fclose( m ); - std::string s; if( buf ) { s.assign( buf, sz ); std::free( buf ); } - return s; + return rw::renderToString( std::forward( emit ), "pack-task: open_memstream failed — section skipped from the budget" ).text; } // R2: section 1 as ONE cohesive unit — the distance-masked packSignatures call (eligibleIds only) PLUS the @@ -1130,7 +1215,8 @@ inline std::vector selectMonotoneBodySubset( const IngestResult& ing, co // PRE-budget-trim surface (a trimmed tail names slightly fewer) — an honest ceiling, documented as one. inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, const std::string& task, const LensRanking& lr, const PackTaskInputs& inArg, - std::string* jsonOut = nullptr, std::vector* surfaceOut = nullptr ) + std::string* jsonOut = nullptr, std::vector* surfaceOut = nullptr, + std::size_t* testsKeptOut = nullptr ) { // P2.4 — reuse-count self-supply. --pack-task's CLI/MCP call-sites only compute fan-in when --for or // --metrics was ALSO given, so the bundle used to print in="0" on every row while --for reported the real @@ -1371,8 +1457,10 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, remaining = remaining > notesRoll.charge ? remaining - notesRoll.charge : 0; // ── section 5 — tests_to_run for the top files (the --affected mining: tests that transitively reach) ─── - std::vector testRows; - std::vector testFiles; // hoisted for the L2 --json tail below + std::vector testFiles; // hoisted for the L2 --json tail below + std::vector ptRows; // the seam's row values — one per test FILE, the unit the section cuts in + std::vector> testPartition; // E1: the KEPT prefix's single/group partition, reused by the JSON tail so both dialects serve the same rows + std::size_t testsBudget = sectionBudget( kPackTaskQuotaTestsPct, carry ); { std::vector testSeeds; for( NodeId b : bodyIds ) @@ -1406,23 +1494,19 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // §A9.5 / §P11.4: the one-call bundle names the tests you owe; it now also names how to RUN them, // from the same TestRunnerIndex --affected / --situ / --test-gate / --exercises read. Built here, // inside the section's scope, because it is lazy — a bundle with no test row reads no runner script. - const rw::TestRunnerIndex runners( ing ); - for( std::uint32_t f : testFiles ) + // §B14 — std::string rows, not char[512]: a row carries TWO unbounded interpolands (the test path AND + // the runner command), so it was the widest of the six breaching sites. + const std::string ptPrefix = in.rootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( in.rootArg ); + ptRows = rw::testRowsOutOf( testFiles, [ & ]( std::uint32_t f ) -> std::string_view { - // §B14 — std::string, not char[512]. This row carried TWO unbounded interpolands (the test path - // AND the runner command), so it was the widest of the six breaching sites. - const std::string_view rp = in.rootArg.empty() ? std::string_view( ing.files[f] ) : rw::sarif::rootRelativeUri( ing.files[f], rw::sarif::rootPrefixOf( in.rootArg ) ); - std::string row = " testsRoll.charge ? remaining - testsRoll.charge : 0; @@ -1494,7 +1578,14 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, } reflow = reflowListSection( callers, "callers", callersAttr, callerRows, callersBudget, kPackTaskWrapReserveWide, reflow ); reflow = reflowListSection( notes, "notes", "", noteEntries, notesBudget, kPackTaskWrapReserve, reflow ); - reflow = reflowListSection( tests, "tests", "", testRows, testsBudget, kPackTaskWrapReserve, reflow ); + // The tests section's own reflow lap — reflowListSection's rule (top up a section the first lap capped, + // once, and pass nothing further on) over packTaskTestsSection's cut. It is the LAST section to reflow, so + // its leftover goes nowhere and is not computed. + if( reflow > 0 && tests.kept < testsTotal ) + { + testsBudget += reflow; + tests = packTaskTestsSection( runners, ptRows, testsBudget, kPackTaskWrapReserve, ex, &testPartition ); + } const std::string& callersStr = callers.xml; const std::size_t callersKept = callers.kept; @@ -1637,16 +1728,20 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, } j += "]"; - const std::size_t testsShown = std::min( testsKept, testFiles.size() ); - { char b[ 96 ]; rw::formatTo( b, sizeof( b ), ",\"tests_total\":{},\"tests_kept\":{},\"tests_to_run\":[", testsTotal, testsShown ); j += b; } + // E1: tests_total/tests_kept count test FILES exactly as the XML section's total=/shown= do — one entry + // per file on both sides of the cut. The JSON tail renders the SAME kept prefix through the SAME + // partition the XML body was grouped by, so the two dialects cannot list different rows. + { 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 auto jrun = [ & ]( std::string_view s ) { return jsonStr( s ); }; - for( std::size_t i = 0; i < testsShown; ++i ) + const rw::TestRunnerIndex jsonRunners( ing ); + 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; + for( const rw::RenderedTestRow& r : rw::testRowsRendered( jsonRunners, jKeptRows, rw::TestRowShape{ rw::RowDialect::Json, "p" }, jrun, &testPartition ) ) { - j += std::string( i == 0 ? "" : "," ) + "{\"p\":\"" + jsonStr( std::string( jPathRel( testFiles[i] ) ) ) + "\"" - + rw::runFieldJsonDisclosed( jsonRunners, testFiles[i], jrun ) + "}"; + j += std::string( jFirstTest ? "" : "," ) + r.text; + jFirstTest = false; } j += "]"; @@ -1681,7 +1776,7 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, report += "bodies: " + listStatus( bodiesTotal, bodiesStr, bodiesKept ) + ( bodiesTotal > 0 && !bodiesStr.empty() && bodiesKept < bodiesTotal ? " (capped)" : "" ) + " | "; report += "callers: " + listStatus( callersTotal, callersStr, callersKept ) + " | "; report += "notes: " + listStatus( notesTotal, notesStr, notesKept ) + " | "; - report += "tests: " + listStatus( testsTotal, testsStr, testsKept ); + report += "tests: " + listStatus( testsTotal, testsStr, testsKept ); // E1: test files, as the section's shown=/total= say report += " | far: " + listStatus( farTotal, rankOut.farXml, farKept ); // R2: d2plus name-only tier (nested in ) // A2 (survey card, 2026-09-03) — the pack-task twin of --for's dropped_positive= root fact: how many // rank>0 eligibleIds the section-1 ladder cut. Emitted ONLY when nonzero (the pr_converged precedent, @@ -1714,7 +1809,8 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, droppedPositiveAttr += lr.capAttrs; const PackTaskHeaderParts headerParts{ task, rootOpenStr, taskNote, mentionNote, boostNote, - docMentionNote, sibliftNote, expandNote, report, droppedPositiveAttr, in.rootArg }; + docMentionNote, sibliftNote, expandNote, report, droppedPositiveAttr, in.rootArg, + rw::runHintClauseIfRows( tests.kept ) }; // the ONE gate: the section's own kept count 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 @@ -1805,6 +1901,14 @@ inline std::string packTaskBundleText( const IngestResult& ing, const Graph& g, // §6 --partition: the bundle's own surface (see the contract above). topRanked already contains bodyIds // (bodies are the positive-score head of the SAME order), so the union is topRanked ∪ d2plus ∪ d1. + // E1 / review of #214: the number of test FILES this bundle's section actually kept. A caller + // that must gate the run-hint clause for SEVERAL bundles at once (partition.h's outer legend) needs the + // count, not a substring search over the rendered bytes — `clear(); diff --git a/src/partition.h b/src/partition.h index 351c72b3a..b074714c8 100644 --- a/src/partition.h +++ b/src/partition.h @@ -419,6 +419,7 @@ struct BundleOut std::vector surface; std::uint32_t assigned = 0; std::uint32_t modules = 0; + std::size_t testsKept = 0; // E1: test FILES this bundle's section kept — the outer legend's gate }; // the fixed context every bundle render shares — grouped (not individual params) so renderMaskedBundle stays @@ -449,7 +450,7 @@ inline BundleOut renderMaskedBundle( const BundleRenderCtx& ctx, const std::vect in.innerBundle = true; // P10 (L7): one outer legend for the whole document in.rankTopN = std::min( std::size_t( kPackTaskRankTopN ), keep.size() ); // never widen the window past the slice - out.xml = packTaskBundleText( *ctx.ing, *ctx.g, *ctx.task, masked, in, ctx.wantJson ? &out.json : nullptr, &out.surface ); + out.xml = packTaskBundleText( *ctx.ing, *ctx.g, *ctx.task, masked, in, ctx.wantJson ? &out.json : nullptr, &out.surface, &out.testsKept ); std::sort( out.surface.begin(), out.surface.end() ); out.surface.erase( std::unique( out.surface.begin(), out.surface.end() ), out.surface.end() ); return out; @@ -593,7 +594,23 @@ inline std::string packTaskPartitionText( const IngestResult& ing, const Graph& whole += partitionSummaryAttrs( plan, sum, ov ); whole += ">"; whole += kPartitionLegend; - whole += ""; // P10 (L7): stated once + // M21(b)/E1: the / row rule rides the outer legend ONCE for every slice, and only when some slice + // kept a test row — the same rows-gating the single bundle applies to its own header; test/partitioncheck.sh + // P10 holds the two within 1.3x. + // + // Review of #214: this used to ASK the rendered bytes (`xml.find( "/ row + bool rendered = true; // false ⇒ the measurement buffer failed; `body` is empty and means nothing }; // ONE estimator for every --pr-context root, never two counters (serialize.h's standing rule, the one @@ -533,6 +535,25 @@ struct PrTrimRender // The floor-exceeded suffix feeds back into the price (it lengthens truncated=, hence the root tag), so it // is applied and RE-PRICED: monotone, since adding bytes to a document already over budget cannot bring it // under, so one re-price is the fixpoint and the printed number is the document's real price either way. +// One level's body, plus the two facts a caller cannot recover from the bytes: how many test FILES it +// rendered (E1 — the run-hint clause is gated on that count, never on a string match over the body) and +// whether the measurement buffer opened at all. Through infra/emit.h's ONE renderToString seam, so a failure +// is ALERTED rather than returned as an indistinguishable empty body. +template< typename EmitFn > +inline PrTrimRender prRenderLevel( const EmitFn& emitFiles, const PrTrim& trim ) +{ + PrTrimRender out; + const rw::Rendered r = rw::renderToString( [ & ]( std::FILE* ms ) { emitFiles( ms, trim, &out.testFiles ); }, + "pr-context: open_memstream failed — this level was not measured" ); + out.body = r.text; + out.rendered = r.ok; + if( !r.ok ) + { + out.testFiles = 0; + } + return out; +} + template< typename EmitFn, typename PriceFn > inline PrTrimRender pickPrTrimLevel( const EmitFn& emitFiles, std::size_t budgetTokens, const PriceFn& price, const std::string& windowAttrs ) @@ -541,24 +562,30 @@ inline PrTrimRender pickPrTrimLevel( const EmitFn& emitFiles, std::size_t budget PrTrimRender out; for( std::size_t li = 0; li < nLevels; ++li ) { - char* buf = nullptr; - std::size_t sz = 0; - std::string rendered; - if( std::FILE* ms = open_memstream( &buf, &sz ) ) - { - emitFiles( ms, kPrTrims[li] ); - std::fflush( ms ); - std::fclose( ms ); - if( buf ) - { - rendered.assign( buf, sz ); - } - } - std::free( buf ); + const PrTrimRender probe = prRenderLevel( emitFiles, kPrTrims[li] ); + out.body = probe.body; + out.testFiles = probe.testFiles; + out.rendered = probe.rendered; out.level = li; out.truncated = li > 0 ? std::string( kPrTrims[li].dropped ) : std::string( "none" ); - out.body = std::move( rendered ); - out.estTokens = price( out.body.size(), li, out.truncated, windowAttrs ); + out.estTokens = price( out.body, out.testFiles, li, out.truncated, windowAttrs ); + if( !probe.rendered ) + { + // THIS LEVEL WAS NOT MEASURED, AND THE DOCUMENT MUST SAY SO. A failed render leaves probe.body + // EMPTY, the price of an empty body fits any budget, and the ladder therefore breaks here with a + // root that prints est_tokens = the price of nothing while writePrContext streams the COMPLETE + // floor level. The bytes are the right answer — cutting rows because a measurement buffer failed + // would make a cap decide the content, which is the one thing a cap may never do — but the + // NUMBER is modelled, and until now its only signal was DEGRADED_PATH_ALERT, which Diagnostics.h + // compiles to `do {} while (0)` under NDEBUG. So the shipped binary printed a wrong est_tokens + // with no disclosure at all (review of #214). truncated= is the attribute that already carries + // exactly this class of fact, so the fact goes there and survives the flavour. + // + // Re-priced after the label is appended, for the same reason the budget-floor rung below + // re-prices: the label lengthens the root tag, so a number printed beside it must include it. + out.truncated += ";est-unmeasured"; + out.estTokens = price( out.body, out.testFiles, li, out.truncated, windowAttrs ); + } if( out.estTokens <= budgetTokens ) { break; @@ -566,7 +593,7 @@ inline PrTrimRender pickPrTrimLevel( const EmitFn& emitFiles, std::size_t budget if( li + 1 == nLevels ) { out.truncated += ";budget-floor-exceeded"; // even the floor render is over budget - out.estTokens = price( out.body.size(), li, out.truncated, windowAttrs ); + out.estTokens = price( out.body, out.testFiles, li, out.truncated, windowAttrs ); } } return out; @@ -577,7 +604,14 @@ inline PrTrimRender pickPrTrimLevel( const EmitFn& emitFiles, std::size_t budget inline std::string prBudgetTail( std::size_t changedFiles, std::uint32_t skippedModeOnly, std::size_t budgetTokens, const PrTrimRender& chosen, const std::string& truncatedEscaped ) { - char tail[ 256 ]; + // 320, not 256: test/fixedbufsweep.sh measured this buffer's worst case at 248 B of 256 — SEVEN bytes of + // margin — and warned that "one more attribute crosses it". ';est-unmeasured' is 15 more and CAN ride + // beside ';budget-floor-exceeded' (a small --max-tokens puts even the empty-body envelope over budget), + // so the worst case is 88 lit + 90 digits + 85 label = 263 B. formatTo is not what was saving it: it + // truncates SILENTLY and its return is not read here, so an overrun would have dropped the closing quote + // of truncated=" and shipped a malformed root — a G4 breach with no diagnostic. The sweep's row moves in + // the same commit with the recomputed number. + char tail[ 320 ]; rw::formatTo( tail, sizeof( tail ), " files=\"{}\" skipped_mode_only=\"{}\" budget_tokens=\"{}\" est_tokens=\"{}\" trim_level=\"{}\" truncated=\"{}\"", changedFiles, skippedModeOnly, budgetTokens, chosen.estTokens, chosen.level, truncatedEscaped.c_str() ); return tail; @@ -597,7 +631,44 @@ inline std::string prBudgetTail( std::size_t changedFiles, std::uint32_t skipped // this comment IS ~91% of that document. Same bytes in the same order; they are simply measured before // they are written, the way every other priced root measures itself (serialize.h §H7). File scope, beside // kPrEmptyDiffBody, so the emitter reads as the decisions it makes rather than as the prose it ships. -inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindexed ) +// E1 (2026-09-12): `withRunClause` splices testmap.h's run=/run_unknown=/ clause. The clause is a rule about +// rows, so it rides only a document whose chosen body renders a / row — decided by the COUNT that +// body's own emitter reported (PrTrimRender::testFiles), never by a search of the rendered bytes. The writer +// builds both forms, the pricer charges runClauseBytes per candidate level from that level's own body, and the +// form matching the chosen body is written — after the choice, since the legend precedes the root in the +// stream but not in the decision. A corpus-level predicate ("the corpus holds a test file") over-approximated +// (CodeRabbit on #214): a test elsewhere in the corpus, or a testCap=0 level, bought the clause for a document +// with no row. Measured on test/defaultceilingcheck.sh's 120-file, no-test fixture: unconditional, 7,989 -> +// 8,025 tokens, over the 8,000 default budget; gated, 7,989. +// Review of #214: the est-unmeasured label's definition, and the ONE wording that defines it — the legend +// clause for the disclosure that replaces an alert the release build compiles out. +// +// CHARGED LIKE kRunHintLegendClause, AND FOR THE SAME REASON. This is a rule about a label, so it rides only +// a document whose chosen level actually carries that label — decided by the FACT the ladder recorded +// (PrTrimRender::rendered), never by a search of the rendered bytes. Unconditional, it cost the +// test/defaultceilingcheck.sh fixture its whole remaining headroom: that 120-file tree prices at 7,989 of +// the 8,000 default (11 tokens spare, as E1 measured when it gated the run clause for the same reason) and +// went to 8,037 — over budget, on a document with nothing unmeasured about it. Gated, it is 0 B there and +// the pricer charges it exactly on the level that states it, so the priced and the delivered legend cannot +// disagree. Defined-wherever-emitted is the rule prbudgetcheck (#10) already holds budget-floor-exceeded to; +// test/prcontextcheck.sh (F6) holds this one to it. +inline constexpr std::string_view kPrEstUnmeasuredLegendClause = + "truncated= carrying est-unmeasured means the chosen level could not be MEASURED (its measurement buffer, or the copy out of it, failed), so est_tokens= is a MODELLED " + "number and not this document's own price — recounting the delivered bytes will NOT reproduce it. The BYTES are unaffected: the complete untrimmed level is served, " + "because a failed measurement may not decide what the answer contains. "; + +// The two CONDITIONAL clauses of this legend, named instead of passed as a pair of bare bools: the call site +// `prLegendText( escBase, unindexed, true, false )` says nothing about which clause is which, and the two are +// 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. +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 +}; + +inline std::string prLegendText( const std::string& baseEscaped, bool hasUnindexed, const PrLegendClauses& clauses ) { return std::string( ""; + + 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 + + std::string( clauses.estUnmeasured ? kPrEstUnmeasuredLegendClause : std::string_view() ) // review of #214: the est-unmeasured label's definition — label-gated, same reason + + "-->"; } inline constexpr std::string_view kPrEmptyDiffBody = @@ -658,13 +732,13 @@ inline std::string prEmptyRootTail( std::uint32_t skippedModeOnly, std::size_t b template< typename PriceFn > inline std::pair prEmptyRootPrice( const PriceFn& price, std::size_t budgetTokens ) { - const std::size_t plain = price( kPrEmptyDiffBody.size(), 0, std::string( "none" ), std::string() ); + const std::size_t plain = price( kPrEmptyDiffBody, 0, 0, std::string( "none" ), std::string() ); if( budgetTokens == 0 || plain <= budgetTokens ) { return { plain, std::string( "none" ) }; } const std::string labelled( "budget-floor-exceeded" ); - return { price( kPrEmptyDiffBody.size(), 0, labelled, std::string() ), labelled }; + return { price( kPrEmptyDiffBody, 0, 0, labelled, std::string() ), labelled }; } // Open the root: the attributes EVERY form shares, this site's own tail, and the one remark row @@ -702,7 +776,9 @@ struct PrPriceCtx const PrContextMask* anchor = nullptr; const std::string* baseEscaped = nullptr; const std::string* atAttrs = nullptr; // gitstamp::atAttr, appended past every tail attribute - std::size_t envelopeBytes = 0; // legend + anchoring note + closing tag (never the root tag) + std::size_t envelopeBytes = 0; // legend (WITHOUT the run clause) + anchoring note + closing tag (never the root tag) + std::size_t runClauseBytes = 0; // E1: testmap.h's run=/run_unknown=/ clause, charged only for a body that renders a test row + std::size_t estUnmeasuredClauseBytes = 0; // review of #214: the est-unmeasured clause, charged only for a level that could not be measured std::size_t changedFiles = 0; std::uint32_t skippedModeOnly = 0; std::size_t budgetTokens = 0; @@ -722,10 +798,25 @@ struct PrPriceCtx // // The attribute is part of the document it prices, so its own digits are converged in ≤4 passes exactly as // pricedRootAttr converges them. -inline std::size_t prPriceDocument( const PrPriceCtx& c, std::size_t bodyBytes, std::size_t level, +// E1 (review of #214): the run clause is charged for a level exactly when that level's body RENDERED a test +// row, and the count comes from the emitter that wrote it (PrTrimRender::testFiles), never from a string +// match over the rendered bytes — a body can carry the literal text of a tag inside CDATA or an attribute, +// and a predicate that greps for one is answering a different question than the emitter did. The pricer and +// the writer read the SAME count, so the priced legend and the delivered legend cannot disagree. +inline std::size_t prPriceDocument( const PrPriceCtx& c, std::string_view body, std::size_t testFiles, std::size_t level, const std::string& truncatedEscaped, const std::string& windowAttrs ) { - std::size_t est = 0; + // Review of #214: the est-unmeasured clause is charged off the LABEL this document will print, read from + // the truncated= value the caller already hands in — not off a parallel boolean beside it. The label is + // the fact (pickPrTrimLevel appends it from PrTrimRender::rendered before it prices, wherever it prices), + // so charging on its presence makes the priced legend and the delivered legend impossible to disagree: + // one condition decides both. This is NOT the E1 objection to grepping rendered bytes — truncated= is the + // ladder's own decision string, never emitter output, and the vocabulary that may appear in it is a + // closed const table plus these two labels. + const bool unmeasured = truncatedEscaped.find( "est-unmeasured" ) != std::string::npos; + const std::size_t bodyBytes = body.size() + ( testFiles > 0 ? c.runClauseBytes : 0 ) // E1: the clause rides only a rows-bearing document + + ( unmeasured ? c.estUnmeasuredClauseBytes : 0 ); // review of #214: and this one only a level that was not measured + std::size_t est = 0; for( int pass = 0; pass < 4; ++pass ) { PrTrimRender probe; @@ -899,11 +990,26 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest } std::sort( changed.begin(), changed.end(), [ & ]( std::uint32_t a, std::uint32_t b ) { return ing.files[a] < ing.files[b]; } ); - const std::string legendText = prLegendText( escBase, g.unindexedFiles > 0 ); - std::fwrite( legendText.data(), 1, legendText.size(), out ); - + // 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{} ); const std::string anchorNoteText = prAnchorNoteText( anchorAttr ); - std::fwrite( anchorNoteText.data(), 1, anchorNoteText.size(), out ); + // 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. + // 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. + const auto writeHead = [ & ]( std::size_t testFiles, bool unmeasured ) + { + const std::string legend = ( testFiles > 0 || unmeasured ) + ? prLegendText( escBase, g.unindexedFiles > 0, + PrLegendClauses{ .runHint = testFiles > 0, .estUnmeasured = unmeasured } ) + : legendText; + std::fwrite( legend.data(), 1, legend.size(), out ); + std::fwrite( anchorNoteText.data(), 1, anchorNoteText.size(), out ); + }; // R2/N4: the fixed, non-body envelope of every root this emitter writes — the legend, the anchoring // note, and the closing tag. The root's OWN start tag varies with the tail it carries, so it is @@ -913,10 +1019,12 @@ 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, .changedFiles = changed.size(), .skippedModeOnly = skippedModeOnly, + .envelopeBytes = envelopeBytes, .runClauseBytes = rw::kRunHintLegendClause.size(), + .estUnmeasuredClauseBytes = kPrEstUnmeasuredLegendClause.size(), + .changedFiles = changed.size(), .skippedModeOnly = skippedModeOnly, .budgetTokens = budgetTokens, .isDefaultBudget = budget.isDefault }; - const auto priceOf = [ & ]( std::size_t bodyBytes, std::size_t level, const std::string& truncatedRaw, const std::string& windowAttrs ) - { return prPriceDocument( priceCtx, bodyBytes, level, ex( truncatedRaw ), windowAttrs ); }; + const auto priceOf = [ & ]( std::string_view body, std::size_t testFiles, std::size_t level, const std::string& truncatedRaw, const std::string& windowAttrs ) + { return prPriceDocument( priceCtx, body, testFiles, level, ex( truncatedRaw ), windowAttrs ); }; if( changed.empty() ) { @@ -924,6 +1032,7 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest const std::string rootOpen = prRootOpenText( g, sharedAttrs, prEmptyRootTail( skippedModeOnly, budgetTokens, budget.isDefault, emptyEst, ex( emptyTruncated ) ) + atAttrStr, anchor, escBase ); + writeHead( 0, false ); // the empty-diff body is a fixed comment: no changed file, so no test row and nothing to measure std::fwrite( rootOpen.data(), 1, rootOpen.size(), out ); std::fwrite( kPrEmptyDiffBody.data(), 1, kPrEmptyDiffBody.size(), out ); std::fwrite( kPrCloseTag.data(), 1, kPrCloseTag.size(), out ); @@ -959,7 +1068,10 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // The per-file body emitter, parameterized by a trim level so the budget path can render it at several // depths into a memstream to measure, then re-render the chosen one to `out`. At the deepest trim it is // still one element PER changed file (counts intact) — files are never dropped, only detail is. - const auto emitFilesRange = [ & ]( std::FILE* o, const PrTrim& trim, std::size_t begin, std::size_t end ) + // `testFilesOut` (optional): the number of test FILES the rows below actually rendered, accumulated over + // the range — E1's ONE gate for the run-hint clause, reported BY the emitter instead of grepped back out + // of its bytes. nullptr on the streaming degrade path, which has no legend left to decide. + const auto emitFilesRange = [ & ]( std::FILE* o, const PrTrim& trim, std::size_t begin, std::size_t end, std::size_t* testFilesOut ) { for( std::size_t ci = begin; ci < end; ++ci ) { @@ -1044,11 +1156,12 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest if( trim.testCap > 0 ) { rw::emitTo( o, "", testFiles.size(), tSc.shown, tSc.capped ); - for( std::size_t i = 0; i < tSc.shown; ++i ) - { - rw::emitTo( o, "", ex( prPathRel( testFiles[i] ) ).c_str(), runAttrDisclosed( prRunners, testFiles[i], ex ).c_str() ); // §A9.5 - } + // §A9.5 / E1: the shown window, grouped where no runner is derivable (testmap.h's seam), which + // returns the FILE count with the rows — the number the legend's clause is gated on. + const JoinedTestRows tRows = testRowsList( prRunners, testRowsOutOf( std::span( testFiles ).first( tSc.shown ), prPathRel ), TestRowShape{ RowDialect::Xml, "test" }, ex ); + rw::emitRaw( o, tRows.text.c_str() ); rw::emitRaw( o, "" ); + if( testFilesOut ) { *testFilesOut += tRows.files; } } else { @@ -1172,14 +1285,28 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest // P4 (L7): the changed-file WINDOW — --limit/--offset when given, else every file (the budget may still cut it below) const PageWindow filePw = pageWindow( changed.size(), budget.pageLimit, budget.pageOffset ); std::size_t fileEnd = filePw.end; - const auto emitFiles = [ & ]( std::FILE* o, const PrTrim& trim ) { emitFilesRange( o, trim, filePw.begin, fileEnd ); }; + const auto emitFiles = [ & ]( std::FILE* o, const PrTrim& trim, std::size_t* testFilesOut ) { emitFilesRange( o, trim, filePw.begin, fileEnd, testFilesOut ); }; // NO budget at all (only a multi-root sub-bundle handed 0): level 0, no budget attributes. if( budgetTokens == 0 ) { const std::string rootOpen = prRootOpenText( g, sharedAttrs, " files=\"" + std::to_string( changed.size() ) + "\" skipped_mode_only=\"" + std::to_string( skippedModeOnly ) + "\"" + atAttrStr, anchor, escBase ); + // E1: the head carries a rule about rows, so it follows the body's DECISION even though it precedes + // the body in the stream — the level is rendered into a measurement buffer first and its row count + // decides the legend form. DEGRADE (infra/emit.h renderToString, which alerts): if that buffer cannot + // be opened there is no count, so the clause-less legend is written and the body is STREAMED straight + // to `out` exactly as it was before E1. Complete, correct bytes either way — never an empty body. + const PrTrimRender flat = prRenderLevel( emitFiles, kPrTrims[0] ); + writeHead( flat.testFiles, false ); // this root carries no est_tokens/truncated= at all, so it has no unmeasured price to disclose std::fwrite( rootOpen.data(), 1, rootOpen.size(), out ); - emitFiles( out, kPrTrims[0] ); + if( flat.rendered ) + { + std::fwrite( flat.body.data(), 1, flat.body.size(), out ); + } + else + { + emitFiles( out, kPrTrims[0], nullptr ); + } std::fwrite( kPrCloseTag.data(), 1, kPrCloseTag.size(), out ); return 0; } @@ -1221,8 +1348,20 @@ inline int writePrContext( std::FILE* out, const std::string& root, const Ingest prBudgetTail( changed.size(), skippedModeOnly, budgetTokens, chosen, ex( chosen.truncated ) ) + ( budget.isDefault ? " budget_default=\"1\"" : "" ) + windowAttrs + atAttrStr, anchor, escBase ); + writeHead( chosen.testFiles, !chosen.rendered ); // E1 / review of #214: the legend form the chosen body was priced with, from the same count and the same rendered fact std::fwrite( rootOpen.data(), 1, rootOpen.size(), out ); - std::fwrite( chosen.body.data(), 1, chosen.body.size(), out ); + if( chosen.rendered ) + { + std::fwrite( chosen.body.data(), 1, chosen.body.size(), out ); + } + else + { + // DEGRADE (renderToString alerted): no level could be measured, so est_tokens= is the modelled number + // for an empty body — but the document still owes its bytes. Stream the floor level straight out, the + // same contract serialize.h's ChargedSection degrade keeps: complete, correct bytes, a wrong estimate, + // and an alert saying which. Never an empty . + emitFiles( out, kPrTrims[0], nullptr ); + } std::fwrite( kPrCloseTag.data(), 1, kPrCloseTag.size(), out ); return 0; } diff --git a/src/serialize.h b/src/serialize.h index d15c483b8..37fcd7514 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -1205,22 +1205,15 @@ struct ChargedSection // and sidecar paths, which confounds the very assertion that matters ("the bytes are still complete and // correct"); and it fights the ASan runtime, which this gate must also run under. Scoping the fault to the // est_tokens family is what keeps the assertions clean, so the in-source switch wins on honesty, not effort. -#ifndef NDEBUG +// CA4 w1fix2-verifier G4: this once read `value[0] == '1'`, so `=10`, `=1x` and `=1000000` all injected the +// fault — a prefix test where the contract is a switch. That rule now lives in ONE place, rw::faultSwitchOn +// (infra/emit.h), which every fault switch reads through, so the next one cannot get it wrong again; the +// once-per-process `static` stays here, where determinism needs it. inline bool isChargeBufferFaultInjected() noexcept { - static const bool isOn = []() noexcept - { - // CA4 w1fix2-verifier G4: this read `value[0] == '1'`, so `=10`, `=1x` and `=1000000` all injected the - // fault — a prefix test where the contract is a switch. EXACT "1" is the only ON value; anything else, - // including "0", "true" and the empty string, is OFF. - const char* value = std::getenv( "RIPWIRE_FAULT_CHARGE_BUFFER" ); - return value != nullptr && std::strcmp( value, "1" ) == 0; - }(); + static const bool isOn = rw::faultSwitchOn( "RIPWIRE_FAULT_CHARGE_BUFFER" ); return isOn; } -#else -inline constexpr bool isChargeBufferFaultInjected() noexcept { return false; } -#endif // Drop-in for `open_memstream` at every est_tokens-family measurement buffer. nullptr ⇒ the caller takes its // own documented degrade path; this function never reports a failure it did not have. diff --git a/src/situ.h b/src/situ.h index dbf0a18aa..40bd1de63 100644 --- a/src/situ.h +++ b/src/situ.h @@ -594,17 +594,14 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // 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):\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" ); // §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 ); - for( std::size_t i = 0; i < testRows.size(); ++i ) - { - const TestRow& r = testRows[i]; - const std::string_view rp = situPathRel( r.fileId ); - rw::emitTo( out, " {}{}{}\n", std::string_view( rp.data(), rp.size() ), testRowEvidence( r, EvDialect::Text ).c_str(), - runSuffixTextDisclosed( situRunners, r.fileId ).c_str() ); - } + 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 // runs the compiled BINARY as a subprocess, which is not a call edge, so no test/*.sh gate can EVER be // named above, however much of the change it exercises. Same number, same counter as --affected's @@ -1156,7 +1153,7 @@ 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 ), - tgHasRows ? kTestGateRunLegend : std::string_view{}, + runHintClauseIfRows( testRows ), // 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 @@ -1181,11 +1178,7 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const pagingDisclosure( uab, sizeof( uab ), r.untested.size(), uw.end, pageLimit, pageOffset ), gitstamp::atAttr( root ).c_str(), tgRootAttr.c_str(), nextAttrXml( testGateNextInvocation( ing, r, gateRunners ) ).c_str() ); // P3 (L7) - for( const TestRow& row : r.testRows ) - { - rw::emitTo( out, "", ex( tgPathRel( row.fileId ) ).c_str(), testRowEvidence( row, EvDialect::Xml ).c_str(), - runAttrDisclosed( gateRunners, row.fileId, ex ).c_str() ); - } + rw::emitRaw( out, testRowsJoined( gateRunners, evidenceRowsOut( r.testRows, EvDialect::Xml, tgPathRel ), TestRowShape{ RowDialect::Xml, "t" }, ex ).c_str() ); // E1: where no runner is derivable for( const ShellGateObligation& gate : r.shellGates.obligations ) { rw::emitTo( out, "", ex( tgPathRel( gate.fileId ) ).c_str(), gate.evidence, @@ -1254,11 +1247,7 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co 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 auto jesc = []( std::string_view s ) { return jsonStr( s ); }; - for( std::size_t i = 0; i < r.testRows.size(); ++i ) - { - rw::emitTo( out, "{}{{\"p\":\"{}\"{}{}}}", i == 0 ? "" : ",", jsonStr( tgJPathRel( r.testRows[i].fileId ) ).c_str(), - testRowEvidence( r.testRows[i], EvDialect::Json ).c_str(), runFieldJsonDisclosed( gateRunners, r.testRows[i].fileId, jesc ).c_str() ); - } + 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 ) { const ShellGateObligation& gate = r.shellGates.obligations[i]; diff --git a/src/testmap.h b/src/testmap.h index 316f39785..710576c7d 100644 --- a/src/testmap.h +++ b/src/testmap.h @@ -674,11 +674,294 @@ inline std::string runSuffixTextDisclosed( const TestRunnerIndex& idx, std::uint return suffix.empty() ? std::string( " (run: not derivable)" ) : suffix; } +// ── E1 / A4-2 (output-routing loop, 2026-09-12, owner call) — runner-less rows GROUPED, the disclosure once ── +// On a corpus where almost no harness has a derivable runner (rocksdb: 126 of 127 rows), every row paid the +// same 16 bytes of `run_unknown="1"` (23 in --situ's text) — 2.0–2.5 KB per answer for one fact said 127 +// times. The rows come in EVIDENCE order (changed, partner, hops asc, path), so consecutive runner-less rows +// share their attributes; those are served as ONE row: +// +// XML (the per-row attrs, then n= p=) +// {"p":["a","b","c"],"hops":2,"n":7,"run_unknown":true} JSON ("p" — or "test" — becomes an ARRAY) +// [hops=2] (7): a, b, c (run: not derivable) text +// +// What the grouping may never change — test/testrowruncheck.sh arm 12 proves it on every dialect — is the +// MULTISET of paths: every path verbatim (a reader's grep for a file name still hits; E3's brace-grouped +// directories were disqualified on exactly that), each exactly once, in the order the single rows had. The +// rules, stated once here because they decide every emitter: +// * a row WITH a runner stays a single / row exactly as before — run= is per row; +// * only rows whose remaining per-row attributes are BYTE-EQUAL group (hops=, partner=, changed=, +// seed_kind= — the `attrs` string is the key), so a group never blurs two kinds of evidence; +// * a group is emitted where its FIRST member stood, its members in list order; a single row with a +// runner in the middle of a group stays where it was, so evidence order is preserved row for row; +// * a group of ONE is a single row (the spelling is shorter and a consumer has one less shape); +// * a path containing a ',' is NEVER grouped — it is served as a single row. p= is a comma-separated list +// and every XML parser undoes an entity BEFORE a consumer splits on the delimiter, so an escaped comma +// (this seam spelled , until 2026-09-13) reappears as a separator and n= then disagrees with what +// the reader counts; the text twin had no escape at all. Refusing to group the row is the only spelling +// that is right in all three dialects at once, it costs one row on a path shape that is vanishingly +// rare, and the legend clause says so rather than describing an escape. +// The ""-means-not-derivable test stays in runHint alone: the single rows below go through the Disclosed +// wrappers, and a group exists only where commandFor is empty — one seam, one rule. +// +// The byte cap this seam used to carry (`maxGroupBytes`, a pre-escape estimate of a group's rendered size) +// is GONE, and with it its 48-byte overhead constant and pack-task's per-row units arithmetic. It was the +// wrong depth: the estimate counted UNESCAPED path bytes, so a list of paths holding '&' or '<' rendered +// wider than the cap admitted and packTaskListSection — which breaks at the first over-budget entry — then +// dropped the whole tail of the section, run= singles included. pack-task now CUTS first and GROUPS second +// (packtask.h): the section is cut over single rows, whose rendered bytes are exactly what it measures, and +// the kept prefix is grouped afterwards. Grouping a run of N≥2 rows is strictly smaller than the N single +// rows it replaces (it drops N-1 tag+attribute+disclosure repeats and adds only ` n="N"`), so it can never +// breach a cut that already held. +struct TestRowOut +{ + std::uint32_t fileId = 0; + std::string path; // as the verb spells it (root-relative or not), UNESCAPED + std::string attrs; // the per-row attributes in the dialect's spelling (testRowEvidence + seed_kind=), possibly empty — the group key +}; + +enum class RowDialect : std::uint8_t { Xml, Json, Text }; + +struct TestRowShape +{ + RowDialect dialect = RowDialect::Xml; + std::string_view tag = "t"; // XML element name ("t" | "test") or JSON key ("p" | "test") + std::string_view indent = ""; // text dialect: the line prefix +}; + +// One rendered row: the text, and how many test FILES it carries (1 for a single row, n for a group), so a +// caller counting files (pack-task's kept/shown arithmetic) never mistakes rows for tests. +struct RenderedTestRow +{ + std::string text; + std::uint32_t files = 1; +}; + +// The partition: index lists into `rows`, a run of one for a single row, a run of ≥2 for a group. +// +// A group covers a CONTIGUOUS run only: the scan stops at the first row that is not a groupable row with the +// same attrs. The rows arrive in evidence order, so equal-attribute groupable rows are already adjacent and +// the only things that can interrupt a run are a same-attribute row WITH a runner and a path carrying a ','; +// hoisting the rows after it into a group in FRONT of it would move them ahead of it (review of #214: +// A, B(run), A became G(A,A), B). Stopping instead costs one more per interruption and makes order +// preservation true by construction — test/testrowruncheck.sh arm 12 reads the paths back in emitted order +// and asserts they are the single rows' order. Linear: `i` advances to the end of the run it just closed, so +// every row is visited exactly once and no `taken` bookkeeping is needed to find the next unconsumed row. +inline std::vector> partitionTestRows( const TestRunnerIndex& idx, std::span rows ) +{ + std::vector> groups; + // The two disqualifications, in one place: a derivable runner (run= is per row) and a ',' in the path + // (p= is a comma-separated list — see the seam's header comment for why no escape can rescue it). + const auto groupable = [ & ]( std::uint32_t i ) noexcept + { + return idx.commandFor( rows[i].fileId ).empty() && rows[i].path.find( ',' ) == std::string::npos; + }; + for( std::uint32_t i = 0; i < rows.size(); ) + { + std::uint32_t end = i + 1; + if( groupable( i ) ) + { + while( end < rows.size() && groupable( end ) && rows[end].attrs == rows[i].attrs ) + { + ++end; + } + } + std::vector members; + members.reserve( end - i ); + for( std::uint32_t k = i; k < end; ++k ) + { + members.push_back( k ); + } + groups.push_back( std::move( members ) ); + i = end; + } + return groups; +} + +// A single row, in the dialect — the disclosure through the Disclosed wrappers above, never re-spelled. +template +inline std::string renderSingleTestRow( const TestRunnerIndex& idx, const TestRowOut& r, const TestRowShape& shape, EscapeFn esc ) +{ + std::string s; + switch( shape.dialect ) + { + case RowDialect::Xml: + s += "<"; s.append( shape.tag ); s += " p=\""; s += esc( r.path ); s += "\""; s += r.attrs; s += runAttrDisclosed( idx, r.fileId, esc ); s += "/>"; + break; + case RowDialect::Json: + s += "{\""; s.append( shape.tag ); s += "\":\""; s += esc( r.path ); s += "\""; s += r.attrs; s += runFieldJsonDisclosed( idx, r.fileId, esc ); s += "}"; + break; + case RowDialect::Text: + s.append( shape.indent ); s += r.path; s += r.attrs; s += runSuffixTextDisclosed( idx, r.fileId ); s += "\n"; + break; + } + return s; +} + +// A group row (≥2 members, no runner by construction), in the dialect. +template +inline std::string renderTestRowGroup( std::span rows, std::span members, const TestRowShape& shape, EscapeFn esc ) +{ + VERIFY( members.size() >= 2 ); + const TestRowOut& first = rows[ members[0] ]; + std::string s; + switch( shape.dialect ) + { + case RowDialect::Xml: + { + s += ""; + break; + } + case RowDialect::Json: + { + s += "{\""; s.append( shape.tag ); s += "\":["; + for( std::size_t k = 0; k < members.size(); ++k ) + { + if( k ) { s += ','; } + s += '"'; s += esc( rows[ members[k] ].path ); s += '"'; + } + s += "]"; s += first.attrs; s += ",\"n\":"; s += std::to_string( members.size() ); s += ",\"run_unknown\":true}"; + break; + } + case RowDialect::Text: + { + s.append( shape.indent ); + // attrs are built with a LEADING space so every other dialect can append them straight after a + // tag name; this dialect STARTS a line with them, so that one space is dropped. A view, not a + // substr copy. (Deliberately not lintrules.h's ltrim: that header is the --lint verb's rule table + // and pulls ingest.h in with it — this shared emit seam must not depend on a verb.) + if( !first.attrs.empty() ) + { + std::string_view a( first.attrs ); + if( a.front() == ' ' ) { a.remove_prefix( 1 ); } + s.append( a ); s += ' '; // " [hops=2]" -> "[hops=2] " + } + s += '('; s += std::to_string( members.size() ); s += "): "; + for( std::size_t k = 0; k < members.size(); ++k ) + { + if( k ) { s += ", "; } + s += rows[ members[k] ].path; + } + s += " (run: not derivable)\n"; + break; + } + } + return s; +} + +// The two ways a caller has its rows: a bare file list (--exercises' seeds, --pr-context, --handoff, --flags +// --flip, --pack-task, situational_awareness — no per-row attributes), or rankTestRows' evidence rows +// (--situ's three dialects). ONE builder each, so six sites do not carry six copies of the same loop. +template +inline std::vector testRowsOutOf( std::span files, PathFn pathRel ) +{ + std::vector rows; + rows.reserve( files.size() ); + for( std::uint32_t f : files ) + { + rows.push_back( { f, std::string( pathRel( f ) ), {} } ); + } + return rows; +} + +template +inline std::vector evidenceRowsOut( std::span rows, EvDialect d, PathFn pathRel ) +{ + std::vector out; + out.reserve( rows.size() ); + for( const TestRow& r : rows ) + { + out.push_back( { r.fileId, std::string( pathRel( r.fileId ) ), testRowEvidence( r, d ) } ); + } + return out; +} + +// THE SEAM every tests_to_run emitter calls (test/testrowruncheck.sh arm 0 censuses its call sites): the +// partition and the rows, in the order the reader gets them. A caller that needs two dialects of ONE +// partition (pack-task's XML section and its JSON tail) passes the partition it already has. +template +inline std::vector testRowsRendered( const TestRunnerIndex& idx, std::span rows, const TestRowShape& shape, EscapeFn esc, + const std::vector>* partition = nullptr ) +{ + const std::vector> own = partition ? std::vector>{} : partitionTestRows( idx, rows ); + const std::vector>& groups = partition ? *partition : own; + std::vector out; + out.reserve( groups.size() ); + for( const std::vector& members : groups ) + { + if( members.size() == 1 ) + { + out.push_back( { renderSingleTestRow( idx, rows[ members[0] ], shape, esc ), 1 } ); + } + else + { + out.push_back( { renderTestRowGroup( rows, members, shape, esc ), std::uint32_t( members.size() ) } ); + } + } + return out; +} + +// The joined form AND the number of test FILES it names, as ONE value. +// +// Review of #214: eight legends gate the run-hint clause below, and each one asked its own question — "is the +// rendered string empty", "does the document contain `` could evict a real +// row to pay for a rule about rows it has none of). The seam that renders the rows is the only thing that +// KNOWS how many there are, so it returns the count with them and every legend asks that one count. +// `files` is the number of test FILES (a row contributes N), and it is 0 exactly when `text` is. +struct JoinedTestRows +{ + std::string text; + std::size_t files = 0; +}; + +template +inline JoinedTestRows testRowsList( const TestRunnerIndex& idx, std::span rows, const TestRowShape& shape, EscapeFn esc, std::string_view sep = {} ) +{ + JoinedTestRows out; + bool first = true; + for( const RenderedTestRow& r : testRowsRendered( idx, rows, shape, esc ) ) + { + if( !first ) { out.text.append( sep ); } + first = false; + out.text += r.text; + out.files += r.files; + } + return out; +} + +// The joined form alone, for the emitters that print the list in one go and count their files elsewhere +// (`sep` between rows: "," for JSON, "" else). +template +inline std::string testRowsJoined( const TestRunnerIndex& idx, std::span rows, const TestRowShape& shape, EscapeFn esc, std::string_view sep = {} ) +{ + return testRowsList( idx, rows, shape, esc, sep ).text; +} + // The ONE sentence every legend that carries a tests_to_run row splices, so the seven cannot drift into // seven wordings of one rule. Deliberately short: it rides on --test-gate's own byte ratchets. +// E1 (2026-09-12): the row is defined in the same sentence, because it is the same rule said once per +// group — and legendcoveragecheck wants n= defined wherever a document carries it. inline constexpr std::string_view kRunHintLegendClause = "run= is the command that discharges a test row; run_unknown=\"1\" means none is derivable for that " - "harness (a guess would be worse than none) — a row carries one or the other, never neither. "; + "harness (a guess would be worse than none) — a or row carries one or the other, never neither. " + " is 2+ runner-less rows with equal attributes served as ONE row: n= how many, p= their paths " + "verbatim in list order — a path holding ',' is never grouped, so p= splits into exactly n= paths. A " + "shown=/total= over these rows counts test FILES: a row is n= of them. "; + +// The clause is a rule about ROWS, so a legend splices it only when the document actually renders one — a +// tests="0" answer pays nothing for it. THE gate, taking the count testRowsList returns (or, for a section +// that cut its own rows, that section's kept count): one rule, one spelling, asked by all eight sites. +inline std::string_view runHintClauseIfRows( std::size_t testFilesRendered ) noexcept +{ + return testFilesRendered == 0 ? std::string_view() : kRunHintLegendClause; +} // ── P9 (capture-audit 2026-09-04) — the tests_to_run row set for ONE changed file ──────────────────── // The FILE reading of --affected, seeded by file id rather than by a path pattern, for callers that already diff --git a/src/verbs_change.h b/src/verbs_change.h index d23c50e24..ccd454762 100644 --- a/src/verbs_change.h +++ b/src/verbs_change.h @@ -123,6 +123,21 @@ std::optional runAffected( const MainDispatch& d ) { return rw::sarif::rootRelativeUri( ing.files[ fileId ], afRootPrefix ); }; + // §P11.4 / E1: the rows are rendered FIRST (testmap.h's seam: run= where a REAL runner is derivable, + // runner-less rows with equal evidence grouped into one row) so the legend below can splice the + // run=/run_unknown=/ clause only when there are rows for it to be a rule about — a tests="0" answer, + // the common clean case, pays nothing for it. The index is constructed here (not hoisted into + // MainDispatch) because it is lazy — a run with no test row reads no script. + const rw::TestRunnerIndex runners( ing ); + std::vector afRows; + afRows.reserve( answer.rows.size() ); + for( rw::TestRow row : answer.rows ) // by value: a matched test file's changed= is spelled seed_kind="test" on this verb + { + const std::uint32_t f = row.fileId; + row.changed = false; + afRows.push_back( { f, std::string( afPathRel( f ) ), std::string( answer.isSeedTestFile[f] ? " seed_kind=\"test\"" : "" ) + rw::testRowEvidence( row, rw::EvDialect::Xml ) } ); + } + const rw::JoinedTestRows afRowsXml = rw::testRowsList( runners, afRows, rw::TestRowShape{ rw::RowDialect::Xml, "test" }, ex ); // seeded_by= is the honesty half of the file-first rule: the two readings answer DIFFERENT questions // over the same argument string and return different counts, so which one fired is a fact about the // measurement, not a detail. seeds= is the resolved seed-symbol count (1 for a lone function, ~84 @@ -134,7 +149,8 @@ std::optional runAffected( const MainDispatch& d ) "script-to-binary edges are NOT modelled, so those gates are invisible to this walk and never counted in tests=/reached=. " "{}" // H2H-Graft F1: the evidence-order clause, testmap.h's ONE wording (changed= is spelled seed_kind="test" here: the argument matched it) "order=evidence says so on the root; partners= counts the partner rows. " - "{}{}-->{}", rw::kTestRowEvidenceLegend, + "{}" // M21(b)/E1: the run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated + "{}{}-->{}", rw::kTestRowEvidenceLegend, rw::runHintClauseIfRows( afRowsXml.files ), // H1: the decl→def residue resolveAffectedSeeds summed over the symbol items. A file:name item whose // definitions were dropped seeded the walk with declarations alone, which reached the reader as a bare // tests="0" — on the verb whose answer is the list of tests to run. Exactly when the root carries it. @@ -148,16 +164,7 @@ std::optional runAffected( const MainDispatch& d ) rw::testRowPartnerCount( answer.rows ), // F1: how many rows stand on the name convention alone or as well afRootAttr.c_str(), // M12: root= says what every below is relative to rw::graphCountFloorAttrXml( g ).c_str() ); // H5/M15: gauge + marker; tests=/reached= are a transitive-caller walk over the name-based CSR - // §P11.4: run= where a REAL runner is derivable, absent where it is not. The index is constructed - // here (not hoisted into MainDispatch) because it is lazy — a run with no test row reads no script. - const rw::TestRunnerIndex runners( ing ); - for( rw::TestRow row : answer.rows ) // by value: a matched test file's changed= is spelled seed_kind="test" on this verb - { - const std::uint32_t f = row.fileId; - row.changed = false; - rw::emitTo( stdout, "", ex( afPathRel( f ) ).c_str(), answer.isSeedTestFile[f] ? " seed_kind=\"test\"" : "", - rw::testRowEvidence( row, rw::EvDialect::Xml ).c_str(), rw::runAttrDisclosed( runners, f, ex ).c_str() ); - } + rw::emitRaw( stdout, afRowsXml.text.c_str() ); // E1: the rows rendered above — runner-less rows with equal evidence as ONE row, the multiset unchanged rw::emitRaw( stdout, "" ); return 0; } @@ -231,11 +238,21 @@ std::optional runExercises( const MainDispatch& d ) // leading dashes (the same reason every other doc comment in the tool writes "quality-delta", not the // flag spelling). xmllint is the gate that catches a regression here. const std::string harnessAttr = exercisesHarnessAttr( ing, sel.testFiles ); // §A9.1, empty for a .cpp/.py harness + // §P11.4 / E1: the seed rows are the tests you are about to re-run — rendered before the legend so the + // run=/run_unknown=/ clause rides only a document that has rows (testmap.h's seam; grouped where no runner is derivable) + const rw::TestRunnerIndex runners( ing ); + const auto exPathRel = [ & ]( std::uint32_t f ) -> std::string_view + { + return exSingleRoot ? rw::sarif::rootRelativeUri( ing.files[f], exRootPrefix ) : std::string_view( ing.files[f] ); + }; + const std::vector exRows = rw::testRowsOutOf( std::span( sel.testFiles ).first( shownSeed ), exPathRel ); + const rw::JoinedTestRows exRowsXml = rw::testRowsList( runners, exRows, rw::TestRowShape{ rw::RowDialect::Xml, "t" }, ex ); rw::emitTo( stdout, "{}", rw::graphCountFloorBrief( g.unindexedFiles > 0 ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str(), rw::rootRelPathsLegend( exSingleRoot ) ); + "{}" // M21(b)/E1: the run=/run_unknown= rule and the group row, testmap.h's ONE wording — rows-gated + "{}{}-->{}", rw::runHintClauseIfRows( exRowsXml.files ), rw::graphCountFloorBrief( g.unindexedFiles > 0 ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str(), rw::rootRelPathsLegend( exSingleRoot ) ); const std::string exRootAttr = exSingleRoot ? ( " root=\"" + ex( cfg.roots[0] ) + "\"" ) : std::string(); rw::emitTo( stdout, "", ex( cfg.exercisesFile ).c_str(), sel.testFiles.size(), shownSeed, @@ -244,12 +261,7 @@ std::optional runExercises( const MainDispatch& d ) + rw::renderDisclosure( prD, rw::DiscloseAs::XmlAttrs ) ).c_str(), exRootAttr.c_str(), rw::graphCountFloorAttrXml( g ).c_str() ); // H5/M15: gauge + marker; reaches= is a transitive-callee walk over the name-based CSR - const rw::TestRunnerIndex runners( ing ); // §P11.4: the seed rows are the tests you are about to re-run - for( std::size_t i = 0; i < shownSeed; ++i ) - { - const std::string_view rp = exSingleRoot ? rw::sarif::rootRelativeUri( ing.files[ sel.testFiles[i] ], exRootPrefix ) : std::string_view( ing.files[ sel.testFiles[i] ] ); - rw::emitTo( stdout, "", ex( rp ).c_str(), rw::runAttrDisclosed( runners, sel.testFiles[i], ex ).c_str() ); - } + rw::emitRaw( stdout, exRowsXml.text.c_str() ); // E1: the seed rows rendered above for( std::size_t i = epw.begin; i < epw.end; ++i ) { const Symbol& s = ing.symbols[ show[i] ]; diff --git a/test/affectedcheck.sh b/test/affectedcheck.sh index 42496158b..039ad8f11 100755 --- a/test/affectedcheck.sh +++ b/test/affectedcheck.sh @@ -69,8 +69,14 @@ printf 'void unrelated_helper() { }\n' > "$R/test/de run(){ perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" "$@" --no-cache 2>/dev/null; } runec(){ perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" "$@" --no-cache >/dev/null 2>"$TMP/err.txt"; } -# extract the basenames of the emitted entries, sorted -tset(){ printf '%s' "$1" | grep -oE ' row's — and a path containing ',' is never grouped (testmap.h +# refuses to, so that p= is one path, not a list), which turned such a row into two names that name nothing. +# The shared reader splits only a QUALIFIED group row and decodes entities, so the two helpers in this file +# can no longer disagree about what a row is. +tset(){ printf '%s' "$1" | python3 "$ROOT/test/testrowpaths.py" paths xml | sed 's|.*/||' | sort | tr '\n' ','; } cnt(){ printf '%s' "$1" | grep -oE 'tests="[0-9]+"' | head -1 | grep -oE '[0-9]+'; } # ── 1) change core.cpp → exactly the two tests that reach its symbols ──────────────────────────────── @@ -268,7 +274,9 @@ done # ── 7) H2H-Graft F1: rows in EVIDENCE order, stem partner first, hops= disclosed ────────────────────── # Ordered basenames (NOT sorted — the order IS the claim). -tord(){ printf '%s' "$1" | grep -oE ' row contributes its members in place) — through the shared +# reader, so this gate and the eight others that ask the same question cannot disagree about what a row is. +tord(){ printf '%s' "$1" | python3 "$ROOT/test/testrowpaths.py" paths xml | sed 's|.*/||' | tr '\n' ','; } D="$( run --affected=src/deep.cpp )" [ "$( tord "$D" )" = "deep_test.cpp,test_zdirect.cpp,test_afar.cpp," ] && [ "$( cnt "$D" )" = 3 ] \ && ok "(7a) --affected=src/deep.cpp: partner first, then hops asc — deep_test, test_zdirect(1), test_afar(2); tests=3" \ @@ -285,7 +293,10 @@ printf '%s' "$D" | grep -q ']*order="evidence"' && printf '%s' "$D" # negative: no stem partner exists for core.cpp, so no row may claim one printf '%s' "$A" | grep -q 'partner="1"' && no "(7f) core.cpp has no *_test partner yet a row claims partner=\"1\"" \ || ok "(7f) partner= never fires without a stem match" -printf '%s' "$A" | grep -q ' group row when a +# sibling shares its evidence, and a single row otherwise — hops="1" is asserted either way. +printf '%s' "$A" | grep -qE ' TERM SAYS WHAT THE FULL CLAUSE SAYS ──────────────────────────────────────────── +# +# THE FINDING (review of 6621370f). testmap.h stopped escaping a comma inside a grouped path — a path holding +# ',' is not grouped at all now — and the FULL clause was rewritten to say so and to state that a shown=/total= +# over these rows counts test FILES. The COMPACT twin of the same rule, compactlegend.h's term, was not: +# it still promised "every path verbatim (, a comma)", an escape the body no longer emits, and it never +# carried the counts-FILES rule. A reader holding only the compact legend was told to undo an entity that is +# not there, and `--affected --legend=compact` on a comma-path corpus contradicted its own rows. +# +# WHY A GATE AND NOT ONE CONSTANT. Every other shared sentence in this tree is ONE constant spliced twice, and +# that is the right shape — but the compact dialect exists precisely to RE-SPELL, not to quote: kRunHintLegendClause +# is 350+ B and the compact term is a 194 B line in a table that is charged per verb. So the two are pinned +# against each other instead, and the REQUIRED FACTS are derived from the full constant rather than typed here: +# a fact is a distinctive phrase the full clause uses, and the compact term must use the same words for it. +# Add a fact to the full clause and this arm fails until the compact term carries it too. +RG_FULL="$( sed -n '/^inline constexpr std::string_view kRunHintLegendClause =/,/;$/p' "$ROOT/src/testmap.h" )" +RG_COMPACT="$( grep -F '"' "$ROOT/src/compactlegend.h" )" +if [ -z "$RG_FULL" ] || [ -z "$RG_COMPACT" ]; then + no "(R) could not read both wordings out of src/ (full=$( printf '%s' "$RG_FULL" | wc -c ) B, compact=$( printf '%s' "$RG_COMPACT" | wc -c ) B)" +else + rgbad="" + # the two facts a consumer cannot act without, in the FULL clause's own words + for fact in "verbatim" "a path holding ','" "splits into exactly n=" "counts test FILES"; do + printf '%s' "$RG_FULL" | grep -qF "$fact" || rgbad="$rgbad [full clause lost the fact: $fact]" + printf '%s' "$RG_COMPACT" | grep -qF "$fact" || rgbad="$rgbad [compact term does not state: $fact]" + done + # and the escape neither may promise again: an XML parser undoes it BEFORE a consumer splits p= on ',' + for w in "$RG_FULL" "$RG_COMPACT"; do + printf '%s' "$w" | grep -qF ',' && rgbad="$rgbad [a wording still promises the , escape testmap.h deleted]" + done + # the compact term stays qualified to and present-only, or --flags' own pays for it + printf '%s' "$RG_COMPACT" | grep -qF 'true, "g"' || rgbad="$rgbad [the compact term lost its element qualifier / present-only flag]" + [ -z "$rgbad" ] \ + && ok "(R) the compact term states every fact the full run-hint clause states, in the same words, and neither promises the deleted , escape" \ + || no "(R) the compact and full readings of have drifted:$rgbad" +fi + [ "$fail" -eq 0 ] && echo 'ALL PASS' || echo 'FAILURES ABOVE' exit "$fail" diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index 5398e43f3..09dafdd39 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -161,12 +161,12 @@ TABLE = { ( "src/mcpverbs.h", "nb" ): ( 7, "safe", "nb[160] x4: the CLI notes' MCP twins, byte-identical format. Plural '' / 's' only." ), ( "src/pageview.h", "buf + written" ): ( 1, "safe", "pageDisclosure's H8 floor marker (capture-audit L4): the %s is syn.floor, one of TWO fixed literals (' counts_floor=\"1\"' 17 B, or its JSON twin ',\"counts_floor\":true' 20 B), appended AFTER the paging snprintf into the SAME caller buffer with the remaining capacity (bufCap - written) as its size, guarded by written < bufCap. Every caller's buffer is sized against kPageDisclosureCap, which the floor literal is part of by construction; nothing user-supplied, nothing escaped." ), # ── src/packtask.h ─────────────────────────────────────────────────────────────────────────────────── - ( "src/packtask.h", "open" ): ( 2, "safe", "open[160] (`%.*s` x2, so INVISIBLE to the pre-wave-3 population, and it is an XML OPEN TAG — the shape §B14 is about): packTaskListSection's ''. Safe by ARITHMETIC, not by shape. 30 B of literal ('<' 1 + ' shown=\"' 8 + '\" total=\"' 9 + '\" capped=\"' 10 + '\">' 2). tag comes from the FOUR call sites (:448 'far', :610 'callers', :659 'notes', :698 'tests') ⇒ 7 B. extraAttr is farAttr[32]/callersAttr[32] or the empty literal, and those two are themselves ' of_top=\"%zu\"' snprintf'd into a char[32] ⇒ 31 B at most. Two %zu ⇒ 20 digits each, %d ⇒ 1. Worst case 30+7+31+20+20+1 = 109 B + NUL against 160: 50 B of margin. NOTE both `.*` precisions are int( v.size() ) — they print a string_view, they do not clamp it; the bound is the caller vocabulary and the char[32] feeding extraAttr. SECOND SITE (2026-08-28 serving-shape round, :903): restatePackTaskBodiesWrapper restates the bodies open tag into its own open[112] — two %zu at 20 digits, a fixed capped literal, and a %s that is the 13 B compress literal or empty, ~35 B of literal in total, worst case 88 B against 111 usable. All-numeric/fixed-vocab, same class as the first site." ), + ( "src/packtask.h", "open" ): ( 3, "safe", "open[160] (`%.*s` x2, so INVISIBLE to the pre-wave-3 population, and it is an XML OPEN TAG — the shape §B14 is about): packTaskListSection's ''. Safe by ARITHMETIC, not by shape. 30 B of literal ('<' 1 + ' shown=\"' 8 + '\" total=\"' 9 + '\" capped=\"' 10 + '\">' 2). tag comes from the THREE call sites (:901 'far', :1385 'callers', :1452 'notes' — 'tests' left this helper in the review of #214, see the third site below) ⇒ 7 B. extraAttr is farAttr[32]/callersAttr[32] or the empty literal, and those two are themselves ' of_top=\"%zu\"' snprintf'd into a char[32] ⇒ 31 B at most. Two %zu ⇒ 20 digits each, %d ⇒ 1. Worst case 30+7+31+20+20+1 = 109 B + NUL against 160: 50 B of margin. NOTE both `.*` precisions are int( v.size() ) — they print a string_view, they do not clamp it; the bound is the caller vocabulary and the char[32] feeding extraAttr. SECOND SITE (review of #214, :420): packTaskTestsSection writes the open tag itself, because that section cuts over its own GROUPED rendering rather than over independent entries. Its own open[160], and the narrowest of the three: the tag is the LITERAL 'tests' (no %.*s at all), so the format is a fixed 35 B ('' 2) plus two %zu at 20 digits and one %d at 1 — worst case 76 B + NUL against 160, 83 B of margin. All-numeric, no caller vocabulary to bound. THIRD SITE (2026-08-28 serving-shape round, :1091): restatePackTaskBodiesWrapper restates the bodies open tag into its own open[112] — two %zu at 20 digits, a fixed capped literal, and a %s that is the 13 B compress literal or empty, ~35 B of literal in total, worst case 88 B against 111 usable. All-numeric/fixed-vocab, same class as the first site." ), # ── src/partition.h ────────────────────────────────────────────────────────────────────────────────── ( "src/partition.h", "h" ): ( 2, "safe", "h[288] x2: ; role is the fixed 'core'/'slice' vocabulary, the rest %zu/%u/%d." ), ( "src/partition.h", "pb" ): ( 1, "safe", "pb[96]: the JSON part header; the %s is '' or ',' (the separator)." ), # ── src/prcontext.h ────────────────────────────────────────────────────────────────────────────────── - ( "src/prcontext.h", "tail" ): ( 1, "latent", "tail[256]: truncated=\"%s\" is ESCAPE-THEN-SNPRINTF in shape, but the value is bounded — kPrTrims[].dropped is a const table (longest 48 B) plus ';budget-floor-exceeded' (22 B), none of which escapes. Worst case 88 lit + 90 digits + 70 = 248 B + NUL against 256: SEVEN bytes of margin. A fifth trim level or one more attribute crosses it." ), + ( "src/prcontext.h", "tail" ): ( 1, "latent", "tail[320]: truncated=\"%s\" is ESCAPE-THEN-SNPRINTF in shape, but the value is bounded — kPrTrims[].dropped is a const table (longest 48 B) plus ';budget-floor-exceeded' (22 B) plus ';est-unmeasured' (15 B), none of which escapes, and the last two CAN co-occur (a small --max-tokens puts even the unmeasured empty-body envelope over budget). Worst case 88 lit + 90 digits + 85 label = 263 B + NUL against 320: 56 B of margin. WAS tail[256] at 248 B — SEVEN bytes — and the review of #214 spent 15 of them on est-unmeasured, which is the 'one more attribute crosses it' this row used to warn about; the buffer moved in the same commit as the label. formatTo was never the thing saving it: it truncates silently and its return is not read here, so an overrun drops the closing quote of truncated=\" and ships a malformed root (a G4 breach with no diagnostic). A sixth trim level or one more label needs this recomputed again." ), # ── src/quality.h ──────────────────────────────────────────────────────────────────────────────────── ( "src/quality.h", "tail" ): ( 2, "not-markup", "tail[96] in shaKeyedCachePath: the qsnap/qheadsnap cache FILENAME; family + two hex digests + %016llx, all fixed-width. tail[64] in rootKeyedCachePath: the lean/rich + mcp cache FILENAME, a literal prefix + the 16-hex root key + a literal suffix — every part a compile-time or fixed-width constant. Neither is emitted." ), # ── src/serialize.h ────────────────────────────────────────────────────────────────────────────────── @@ -431,7 +431,15 @@ if not bad: # format to derive a class from — the same reason nestAttr and escAttr are rows. mentions is +2 because # the comment on that buffer names formatTo as well; arch.h's third mention, its emit.h include line, # predates this change. The code it replaced wrote through emitRaw/emitTo, which this gate does not count. -EXPECTED = { "mentions": 322, "calls": 218, "sites": 218, "rows": 92, "widthforms": 0 } +# 2026-09-14 (review of #214, --pr-context's est-unmeasured disclosure): mentions 323 -> 324, +# calls/sites/rows/widthforms ALL UNCHANGED at 219/219/92/0 — re-read from `git diff` and not +# accepted from the delta. The single added line is a COMMENT, not a call: prBudgetTail's buffer +# grew 256 -> 320 for the new ';est-unmeasured' label (its TABLE row above carries the recomputed +# worst case, 248 -> 263 B, margin 7 -> 56) and the comment explaining the growth names formatTo, +# because formatTo's SILENT truncation is precisely what was not saving the old bound — it writes +# at most cap-1 and its return is not read there, so an overrun would have dropped the closing +# quote of truncated=" and shipped a malformed root. Same buffer, same one call, same one row. +EXPECTED = { "mentions": 324, "calls": 219, "sites": 219, "rows": 92, "widthforms": 0 } # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the # H9 ` max_tokens="%d"` ceiling disclosure: a single %d of a caller-supplied INTEGER, no %s, diff --git a/test/impactpartitioncheck.sh b/test/impactpartitioncheck.sh index 70858016b..fd5b99bfb 100644 --- a/test/impactpartitioncheck.sh +++ b/test/impactpartitioncheck.sh @@ -48,8 +48,10 @@ BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" cd "$ROOT" -python3 - "$BIN" <<'PY' -import re, subprocess, sys +ROOT="$ROOT" python3 - "$BIN" <<'PY' +import os, re, subprocess, sys +sys.path.insert(0, os.path.join(os.environ.get("ROOT", "."), "test")) +import testrowpaths # THE shared tests_to_run row reader BIN = sys.argv[1] fail = [0] @@ -106,7 +108,11 @@ if dup_count: print(" INFO --test-gate=%s: %d row(s) share a (name,file) key with another row (no line= on to disambiguate) — collapses identically on both sides" % (SAMPLE_FILE, dup_count)) ok("--test-gate=%s: parsed %d rows into %d distinct (name,file) keys (shown_untested=%s)" % (SAMPLE_FILE, len(testgate_untested_raw), len(testgate_untested), tg_attrs.get("shown_untested"))) -testgate_testfiles = set(norm_path(m.group(1)) for m in re.finditer(r'`), +# and this set was built from the single rows alone — on a corpus where the rows group, a grouped test file +# was missing from the set and its row was then counted as an untested one. Read through the shared +# reader (test/testrowpaths.py), which knows both shapes in every dialect. +testgate_testfiles = set(norm_path(p) for p in testrowpaths.xml_paths(tg)) # ── union every --impact=src/graph.h:SYM call's rows, keyed (name, normalized path) -> tested bool ───── impact_rows = {} # key -> tested (bool) diff --git a/test/listingpagingcheck.sh b/test/listingpagingcheck.sh index 6446f227d..f105cd4ce 100755 --- a/test/listingpagingcheck.sh +++ b/test/listingpagingcheck.sh @@ -346,7 +346,9 @@ case "$FP_NEXT" in esac # ANSWER: the rows equal the tests= total at the DEFAULT cap T_TOTAL="$( attr "$TMP/fp_def" flip tests )" -T_ROWS="$( countrows fp_def t )" +# E1 (2026-09-12): runner-less rows with equal attributes ride ONE row, so the answer is +# counted in test FILES: the single rows plus every row's n=. +T_ROWS="$( { countrows fp_def t; grep -oE ']*/>' "$TMP/fp_def" | grep -oE ' n="[0-9]+"' | grep -oE '[0-9]+'; } | awk '{ s += $1 } END { print s + 0 }' )" if [ "${T_TOTAL:-0}" = "$T_ROWS" ]; then ok "(C) answer: all ${T_TOTAL:-0} tests_to_run rows ride the default page — never windowed" else no "(C) answer: tests=\"$T_TOTAL\" but $T_ROWS rows at the default cap — the ANSWER rows are being paged"; fi @@ -395,7 +397,12 @@ for text, which in ( ( d, "bare" ), ( a, "--limit=1000000" ) ): hdr = [ l for l in text.splitlines() if l.startswith( " [2] tests to run" ) ] line = hdr[ 0 ] if hdr else "" total = int( re.search( r'\((\d+)\)', line ).group( 1 ) ) if re.search( r'\((\d+)\)', line ) else 0 - served = len( rows( text, " [2] tests to run", r'^ \S' ) ) - 1 # minus the script-gates disclosure line + # E1 (2026-09-12): a `[hops=N] (n): a, b, …` line serves n files on one line — count FILES, not lines + served = 0 + for r in rows( text, " [2] tests to run", r'^ \S' ): + gm = re.match( r'^ (?:\[[^\]]*\] )*\((\d+)\): ', r ) + served += int( gm.group( 1 ) ) if gm else 1 + served -= 1 # minus the script-gates disclosure line if total <= 25: print( " FAIL (D) answer: only %d test rows (%s) — the fixture cannot show the retired 25-row cap is gone" % ( total, which ) ); fail = 1 elif "showing" in line or "capped=" in line: diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index b3ac77d32..248141f43 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -209,7 +209,26 @@ tools = json.loads( line )[ "result" ][ "tools" ] # this twin. Its description gained NO prose: a first draft named coverage= and the page there (+109 B) and was # removed rather than re-anchored around, the L7 precedent above — the schema properties are where a client renders # an argument, and the answer's own legend defines coverage= and the page. Headroom after this line: 23 B. -CEILING = 42384 +# RE-ANCHORED 2026-09-13 (review of #214): 42,384 → 42,800 = +416 B, EXACTLY the one 207-byte clause plus the +# single space that separates it from the sentence before it (208 B), spliced into EACH of the TWO tool +# descriptions that serve tests_to_run rows as JSON — `situational_awareness` and `explore`; 2 × 208 = 416 +# (measured against a build of ff8d77a1: 42,361 B here, 23 B under the old ceiling). It is NOT the L7 case the +# line above declines. That one removed prose describing an ARGUMENT, because the schema properties are where a +# client renders an argument and the answer's own legend defines the rest. This clause describes the RESPONSE: +# E1 made a tests_to_run row's `p` a path string OR an array of paths beside `n`, and these two answers carry +# no legend of any kind — `situational_awareness` returns bare JSON with no vocabulary block — so a caller that +# parses `p` as a string has nowhere else to learn otherwise before it breaks. ONE wording (mcp.h +# kTestRowJsonShapeClause), spliced twice, never a third paraphrase. Headroom after this line: 23 B. +# RE-ANCHORED 2026-09-13 (CodeRabbit on #214): 42,800 -> 43,000 = +200 B for a MEASURED +196, the clause 207 +# -> 305 B plus its one-space separator, in each of the same two descriptions (2 x 98). The first wording +# named the key `p` and only one of the three producers spells it that way: situational_awareness emits +# `test` (mcpverbs.h), explore (packtask.h) and the edit receipt (mcpedit.h) emit `p`. A clause that names +# the wrong key is worse than no clause, because a caller reads it as a contract — so it now names both, per +# producer, and everything the three DO share (string-or-array, `n` beside an array, run/run_unknown) is +# still said once. The quotes around the two keys are SINGLE: this string is spliced straight into the +# tools/list JSON, and the first draft's double quotes made the manifest unparseable — which this gate +# caught as a JSONDecodeError, not as a byte count. Measured 42,973. Headroom after this line: 27 B. +CEILING = 43000 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) diff --git a/test/prcontextcheck.sh b/test/prcontextcheck.sh index 639406996..feadde9ba 100755 --- a/test/prcontextcheck.sh +++ b/test/prcontextcheck.sh @@ -169,6 +169,73 @@ COUT="$( "$BIN" "$CLEAN" --pr-context --no-cache 2>/dev/null )"; CRC=$? && ok "clean tree degrades (files=0, exit 0)" \ || no "clean-tree degrade wrong (rc=$CRC): $COUT" +# ── E1 (2026-09-12): the run=/run_unknown=/ clause rides only a document whose corpus CAN carry a test row ─ +# The legend is written and priced before the files are rendered (the budget ladder fits est_tokens= to +# the envelope), so this bundle cannot gate the clause on the rows it ends up emitting the way --affected +# does. It gates on the one fact known before rendering that decides whether a / row is possible +# at all: does the corpus hold a test file. A corpus without one paid 330 B for a rule about rows it can +# never emit — measured on test/defaultceilingcheck.sh's 120-file fixture: est_tokens 7,989 -> 8,025, +# over the 8,000-token default budget. Red on the pre-fix binary (the clause rode every bundle). +{ echo "$OUT" | grep -q 'run_unknown=' && echo "$OUT" | grep -q ' definition in its legend" \ + || no "run clause: the fixture has test/test_core.cpp yet the legend does not define run_unknown=/" +NT="$TMP/notests"; mkdir -p "$NT/src"; git -C "$NT" init -q +git -C "$NT" config user.email a@x.com; git -C "$NT" config user.name A +printf 'int g( int x ) { return x; }\n' >"$NT/src/a.cpp"; git -C "$NT" add -A; git -C "$NT" commit -qm init +printf 'int h( int x ) { return g( x ) + 1; }\n' >>"$NT/src/a.cpp" +NTOUT="$( "$BIN" "$NT" --pr-context --no-cache 2>/dev/null )" +if echo "$NTOUT" | grep -q 'files="1"'; then + echo "$NTOUT" | grep -q 'run_unknown=' \ + && no "run clause: a corpus with NO test file still pays for the run=/run_unknown=/ clause" \ + || ok "run clause: a corpus with no test file carries no run=/run_unknown=/ clause (nothing it can be a rule about)" +else + no "run clause: the no-test fixture did not produce a one-file bundle: $( echo "$NTOUT" | head -c 300 )" +fi + +# ── E1 follow-up (CodeRabbit on #214, third thread): the clause follows the RENDERED rows, not the corpus ── +# "the corpus holds a test file" over-approximated: a test file elsewhere in the corpus, or a trim level whose +# testCap is 0, still bought the clause for a document that renders no test row. The legend is now built after +# the level is chosen and priced per candidate level from that level's OWN ROW COUNT — the emitter reports how +# many test files it wrote (PrTrimRender::testFiles) and both the pricer and the writer read that one number. +# A first fix grepped the rendered body for a `"$OT/src/a.cpp" +printf 'int gb( int x ) { return x * 2; }\n' >"$OT/src/b.cpp" +printf 'int gb( int x );\nint test_gb( void ) { return gb( 1 ); }\n' >"$OT/test/t_other.cpp" +git -C "$OT" add -A; git -C "$OT" commit -qm init +printf 'int ha( int x ) { return ga( x ) + 1; }\n' >>"$OT/src/a.cpp" +OTOUT="$( "$BIN" "$OT" --pr-context --no-cache 2>/dev/null )" +# rows live in the BODY, after the root's start tag — the legend's own `` definition must not read as a row +OTBODY="${OTOUT#* sections in PATH order, so on this repo @@ -295,4 +362,279 @@ else ok "P10.4 invariant: dependents>0 always implies files>0 (files= is the reached total)" fi +# ── (F) THE EMITTER THROWS: renderToString releases what it owns, discloses, and the document still ships ── +# +# THE FINDING (CodeRabbit on #214, src/infra/emit.h). `emit( m )` was called outside any handler. A throw +# from it — std::bad_alloc out of the std::format fallback is the reachable one — skipped the fclose, the +# free, the alert and the documented empty-result fallback in one jump: the memstream and its buffer leaked +# and the caller got an exception where its contract says it gets ok == false. +# +# A throw path is unreachable from a gate by ordinary means, so this drives the in-source fault switch +# INFRA_FAULT_RENDER_EMIT_THROW=1 — serialize.h's isChargeBufferFaultInjected idiom (the INFRA_ prefix, not +# this project's, because src/infra/ is built to travel and test/infraportcheck.sh (C) refuses a layer file +# that names the host — it caught this switch's first spelling), and therefore living +# ONLY on the non-NDEBUG flavour, the same flavour DEGRADED_PATH_ALERT lives on. So, like estchargecheck #14, +# this arm establishes that flavour with its OWN probe rather than assuming it, and must never pass for lack +# of an alert it could not have seen. +# +# THE ARM BRINGS ITS OWN REPOSITORY, because every anchor into the live one is a property of the BRANCH. +# A bare `--pr-context` reads `git diff HEAD`, so on a clean checkout the change set is empty, no trim level +# is ever RENDERED, renderToString is never called, and the arm finds no alert and blames the seam for a +# fixture that asked it nothing. Anchoring at HEAD~3 fixed that locally and broke on CI, which checks out the +# PR's MERGE ref: there HEAD~1 is main's tip and three back is a different set of commits entirely, one that +# named no file this verb reports. Both spellings were the same mistake — test/gatecheck's "the gate fixture +# is the live repo" trap — and counting commits differently would only move it. +# +# So the fixture is BUILT here: a throwaway repo, two commits, one edited file. It is identical on a clean +# checkout, a dirty tree, a merge ref and a shallow clone, because none of those are inputs to it. (The live +# repo is still used for the charge-buffer probe below, which asks the BINARY a question and reads no git +# history at all.) +PRC_FAULT_OUT="$TMP/f_dg.out"; PRC_FAULT_ERR="$TMP/f_dg.err" +PRC_FIX="$TMP/f_repo" +mkdir -p "$PRC_FIX/src" +cat > "$PRC_FIX/src/core.cpp" <<'PRCEOF' +int coreHelper( int y ) +{ + return y + 1; +} + +int coreCompute( int x ) +{ + return coreHelper( x ) * 2; +} +PRCEOF +cat > "$PRC_FIX/src/caller.cpp" <<'PRCEOF' +int coreCompute( int x ); + +int callerEntry( int n ) +{ + return coreCompute( n ) + coreCompute( n + 1 ); +} +PRCEOF +cat > "$PRC_FIX/src/other.cpp" <<'PRCEOF' +int unrelatedLeaf( int z ) +{ + return z - 1; +} +PRCEOF +( + cd "$PRC_FIX" \ + && git init -q \ + && git config user.email gate@example.invalid \ + && git config user.name gate \ + && git add -A \ + && git commit -qm base +) >/dev/null 2>&1 +PRC_FAULT_BASE="$( cd "$PRC_FIX" && git rev-parse HEAD 2>/dev/null )" +# the second commit: ONE file changes, so the range names exactly the file whose row the rows below count +cat > "$PRC_FIX/src/core.cpp" <<'PRCEOF' +int coreHelper( int y ) +{ + return y + 2; +} + +int coreCompute( int x ) +{ + return coreHelper( x ) * 3; +} +PRCEOF +( cd "$PRC_FIX" && git add -A && git commit -qm edit ) >/dev/null 2>&1 +if [ -z "$PRC_FAULT_BASE" ]; then + no "(F) could not build the throwaway git fixture (no base sha) — the emitter-throw arm cannot run" +fi +# +# WHICH FLAVOUR IS THIS BINARY? ASK IT, WITH AN ALERT IT IS KNOWN TO EMIT. +# Both the fault switch and DEGRADED_PATH_ALERT exist only on the non-NDEBUG flavour, so on a Release build +# there is no alert to see and this arm must not read that silence as a regression. The first version of the +# probe settled the question by grepping `--version` for "release" — a LABEL, whose spelling is not this +# gate's to depend on, and which the plain build spells "dev" and the Release build spells neither. It +# answered "this flavour can see alerts" on macOS Release and then failed the SEAM for the missing alert: +# CI red on one job, for a property of the gate, not of the code under test. The binary is asked directly +# now, with the SIBLING fault switch (serialize.h's charge buffer), whose alert is independent of everything +# this arm changes — if that one speaks, this binary can speak, and only then is the emitter-throw alert +# required of it. +INFRA_PROBE_ERR="$TMP/f_probe.err" +RIPWIRE_FAULT_CHARGE_BUFFER=1 "$BIN" "$ROOT/src" --top-k=5 --pack-signatures --no-cache >/dev/null 2>"$INFRA_PROBE_ERR" +if grep -aq 'open_memstream failed' "$INFRA_PROBE_ERR"; then PRC_ALERTS=1; else PRC_ALERTS=0; fi +INFRA_FAULT_RENDER_EMIT_THROW=1 "$BIN" "$PRC_FIX" --pr-context="$PRC_FAULT_BASE" >"$PRC_FAULT_OUT" 2>"$PRC_FAULT_ERR" +prc_f_rc=$? +# and the range must actually name a file, or every assertion below is vacuous +if [ "$( grep -aoc '/dev/null || echo 0 )" = "0" ] && ! grep -aq 'THREW' "$PRC_FAULT_ERR"; then + "$BIN" "$PRC_FIX" --pr-context="$PRC_FAULT_BASE" 2>/dev/null | grep -aq '"$TMP/f_ctl_legend.out" 2>/dev/null +if grep -aq 'est-unmeasured' "$TMP/f_ctl_legend.out"; then + no "(F-legend) an undegraded --pr-context document carries est-unmeasured — either a false disclosure or a clause charged to every reader who does not need it" +else + ok "(F-legend) an undegraded document neither claims est-unmeasured nor pays for its definition (label-gated, like E1's run clause)" +fi + +if [ "$PRC_ALERTS" -eq 0 ]; then + # NO-ALERT FLAVOUR (NDEBUG). The fault switch is `constexpr false` here and the alert macro is compiled + # out, so this arm cannot exercise the degrade at all and must not pretend to: the PLAIN build is what + # proves it (CLAUDE.md). What is still assertable, and worth asserting, is that the verb this arm drives + # is not broken on this flavour — the same document the alerting leg demands, minus the degrade. + printf ' INFO (F) this binary emits no DEGRADED_PATH_ALERT (NDEBUG): the emitter-throw degrade is unobservable BY DESIGN here, and the plain-flavour leg is what proves it\n' + if grep -aq 'renderToString: the emitter THREW' "$PRC_FAULT_ERR"; then + no "(F) a binary that cannot emit the charge-buffer alert emitted the emitter-throw one — the two disagree about this flavour" + else + ok "(F) consistency: no alert on a flavour that compiles them out" + fi + [ "$prc_f_rc" -eq 0 ] \ + && ok "(F1) --pr-context over the fixture repo exits 0 with the (compiled-out) fault requested" \ + || no "(F1) --pr-context exited $prc_f_rc on a flavour where the fault is not even compiled in" + if grep -aq '' "$PRC_FAULT_OUT" && grep -aq ' root ($( wc -c <"$PRC_FAULT_OUT" | tr -d ' ' ) B)" + fi + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$PRC_FAULT_OUT" 2>/dev/null \ + && ok "(F3) the document is well-formed XML (G4 holds)" \ + || no "(F3) the --pr-context document does not parse" + fi + f_rel="$( grep -ao ' row(s) — the verb is intact on this flavour" \ + || no "(F4) the document carries NO row over $PRC_FAULT_BASE" + # (F5) NO FALSE DISCLOSURE. Nothing degraded here (the switch is `constexpr false`), so the est-unmeasured + # label must be ABSENT: a truncation notice on a document that measured its own price would be the + # mirror-image defect of the silence it was added to end. + grep -ao 'truncated="[^"]*"' "$PRC_FAULT_OUT" | grep -aq 'est-unmeasured' \ + && no "(F5) the document claims est-unmeasured on a flavour where the render fault is not compiled in — a disclosure with nothing behind it" \ + || ok "(F5) truncated= carries no est-unmeasured where nothing was left unmeasured" +elif ! grep -aq 'renderToString: the emitter THREW' "$PRC_FAULT_ERR"; then + no "(F) INFRA_FAULT_RENDER_EMIT_THROW=1 produced no DEGRADED_PATH_ALERT on a binary that PROVED it can emit one (the charge-buffer probe alerted) — the seam regressed" +else + ok "(F) observability probe: this binary emits alerts (the charge-buffer fault spoke) and the emitter-throw fault alerts too" + # (F0) the alert names the CAUSE IT HAD. degradeMsg says the BUFFER failed; on this path it did not, so + # reusing it would have been a wrong reason attached to a right consequence. + grep -aq 'open_memstream failed' "$PRC_FAULT_ERR" \ + && no "(F0) the emitter-throw alert blames open_memstream, which did not fail on this path" \ + || ok "(F0) the emitter-throw alert names the throw, not the buffer" + # (F1) THE DOCUMENT STILL SHIPS. The whole point of the degrade: the caller loses the ESTIMATE, never the + # content. prcontext.h streams the floor level straight out when no level could be measured. + [ "$prc_f_rc" -eq 0 ] \ + && ok "(F1) --pr-context over the fixture repo still exits 0 with every render throwing" \ + || no "(F1) --pr-context exited $prc_f_rc with the emitter-throw fault injected — the throw escaped instead of degrading" + if grep -aq '' "$PRC_FAULT_OUT" && grep -aq ' root ($( wc -c <"$PRC_FAULT_OUT" | tr -d ' ' ) B)" + fi + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$PRC_FAULT_OUT" 2>/dev/null \ + && ok "(F3) the degraded document is well-formed XML (G4 holds through the degrade)" \ + || no "(F3) the degraded --pr-context document does not parse — a degrade may not breach G4" + fi + # (F4) and it is not a stub. The direction here is the whole point and is easy to get backwards — this + # arm did, and caught itself: the degrade streams kPrTrims[0], the UNTRIMMED floor, while the + # undegraded run picks whichever level fits its token budget. So the degraded document carries at + # LEAST as many rows as the control and routinely more (40 against 4 on this tree). What the caller + # loses is the ESTIMATE; what it must never lose is content, and "same count" would assert the wrong + # invariant and fail on any tree whose control trims. Compared by ELEMENT COUNT, not bytes. + f_files="$( grep -ao '"$TMP/f_ctl.out" 2>/dev/null + c_files="$( grep -ao ' row — the degrade lost the content it exists to keep" + elif [ "$f_files" -ge "${c_files:-0}" ]; then + ok "(F4) the degraded document carries $f_files row(s) against the control's $c_files — content kept (the floor level is untrimmed), estimate lost" + else + no "(F4) the degraded document carries $f_files row(s), FEWER than the control's $c_files — the degrade lost content, not just the charge" + fi + # (F5) AND THE WRONG NUMBER IS NOW LABELLED. est_tokens= here is the price of an EMPTY body (the ladder + # priced the failed probe and broke at level 0), while the bytes served are the untrimmed floor — + # so the number is modelled, not this document's price. Before the fix the ONLY signal was the alert + # above, which Release compiles out; truncated= is the channel that survives the flavour. + f_trunc="$( grep -ao 'truncated="[^"]*"' "$PRC_FAULT_OUT" | head -1 )" + if printf '%s' "$f_trunc" | grep -aq 'est-unmeasured'; then + ok "(F5) the degraded root DISCLOSES the unmeasured price in truncated= ($f_trunc) — the one channel a Release binary keeps" + else + no "(F5) the degraded root prints an est_tokens priced from an empty body with no est-unmeasured in truncated= ($f_trunc) — a wrong number, silently (non-negotiable #3)" + fi + # (F6) and the label is DEFINED on the document that carries it — prbudgetcheck #10's rule for + # budget-floor-exceeded, applied to the label that now rides beside it. + grep -aq 'est-unmeasured means' "$PRC_FAULT_OUT" \ + && ok "(F6) the degraded document ships the legend clause that defines est-unmeasured" \ + || no "(F6) the degraded document emits est-unmeasured but its own legend never defines the term" +fi + +# ── (G) THE FINAL COPY THROWS: renderToString's last statement was outside its own contract ─────────── +# +# THE FINDING (review of #214, src/infra/emit.h:269). `out.text.assign( buf, sz )` is the one allocation on +# the SUCCESS path, and it sat after the try/catch that arm (F) proves. A std::bad_alloc from it therefore +# escaped renderToString — whose whole documented contract is that a failure is ALERTED and returned as +# ok == false, never thrown — and, jumping over the `std::free( buf )` two lines below, LEAKED the memstream +# buffer on the way out. A no-throw contract with a throwing last statement. +# +# Driven by INFRA_FAULT_RENDER_COPY_THROW=1, the twin of arm (F)'s switch and injected immediately before the +# assign, where a real bad_alloc would land. Same flavour dependence as (F): the switch and the alert both +# live only on the non-NDEBUG build, so the PLAIN-flavour leg is what proves the degrade and the NDEBUG leg +# asserts only what is true there — exactly the structure (F) documents. +PRC_COPY_OUT="$TMP/g_copy.out"; PRC_COPY_ERR="$TMP/g_copy.err" +INFRA_FAULT_RENDER_COPY_THROW=1 "$BIN" "$PRC_FIX" --pr-context="$PRC_FAULT_BASE" >"$PRC_COPY_OUT" 2>"$PRC_COPY_ERR" +prc_g_rc=$? +if [ "$PRC_ALERTS" -eq 0 ]; then + printf ' INFO (G) this binary emits no DEGRADED_PATH_ALERT (NDEBUG): the copy-throw degrade is unobservable BY DESIGN here, and the plain-flavour leg is what proves it\n' + if grep -aq 'renderToString: the final COPY' "$PRC_COPY_ERR"; then + no "(G) a binary that cannot emit the charge-buffer alert emitted the copy-throw one — the two disagree about this flavour" + else + ok "(G) consistency: no alert on a flavour that compiles them out" + fi + [ "$prc_g_rc" -eq 0 ] \ + && ok "(G1) --pr-context exits 0 with the (compiled-out) copy fault requested" \ + || no "(G1) --pr-context exited $prc_g_rc on a flavour where the copy fault is not even compiled in" + g_rel="$( grep -ao ' row(s) — the verb is intact on this flavour" \ + || no "(G2) the document carries NO row over $PRC_FAULT_BASE" +elif ! grep -aq 'renderToString: the final COPY' "$PRC_COPY_ERR"; then + no "(G) INFRA_FAULT_RENDER_COPY_THROW=1 produced no DEGRADED_PATH_ALERT on a binary that PROVED it can emit one (the charge-buffer probe alerted) — the copy is still outside the no-throw contract" +else + ok "(G) the final copy's throw is CAUGHT: the alert speaks instead of the exception escaping" + # (G0) the alert names the cause it HAD. degradeMsg says the BUFFER failed and (F)'s literal says the + # EMITTER threw; here neither did — the copy out of a complete buffer did. + { grep -aq 'open_memstream failed' "$PRC_COPY_ERR" || grep -aq 'the emitter THREW' "$PRC_COPY_ERR"; } \ + && no "(G0) the copy-throw alert blames the buffer or the emitter, neither of which failed on this path" \ + || ok "(G0) the copy-throw alert names the copy, not the buffer and not the emitter" + [ "$prc_g_rc" -eq 0 ] \ + && ok "(G1) --pr-context still exits 0 with every final copy throwing — the throw degraded instead of escaping" \ + || no "(G1) --pr-context exited $prc_g_rc with the copy fault injected — the throw escaped renderToString's no-throw contract" + if grep -aq '' "$PRC_COPY_OUT" && grep -aq ' root ($( wc -c <"$PRC_COPY_OUT" | tr -d ' ' ) B)" + fi + if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$PRC_COPY_OUT" 2>/dev/null \ + && ok "(G3) the degraded document is well-formed XML (G4 holds through the degrade)" \ + || no "(G3) the degraded --pr-context document does not parse — a degrade may not breach G4" + fi + # (G4) the same content-kept/estimate-lost contract arm (F) asserts for its flavour of failure: a failed + # copy is a failed MEASUREMENT, so the floor level streams straight out and the price is disclosed. + g_files="$( grep -ao ' row(s) — content kept, estimate lost" \ + || no "(G4) the degraded document carries NO row — the degrade lost the content it exists to keep" + g_trunc="$( grep -ao 'truncated="[^"]*"' "$PRC_COPY_OUT" | head -1 )" + printf '%s' "$g_trunc" | grep -aq 'est-unmeasured' \ + && ok "(G5) a failed copy is disclosed in truncated= too ($g_trunc) — one label for every unmeasured level" \ + || no "(G5) the copy-degraded root prints an unmeasured est_tokens with no est-unmeasured in truncated= ($g_trunc)" +fi + if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index fb9c3a70d..fa37dbca2 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -18,7 +18,7 @@ path 0 7058d89f7bab7aabe0a5cbf921959bc8a2c346fe65888b776996f879f04a58a2 e3b0c442 connect 0 31c7e3a689a6dcddf5eae17283740823005c3efdfdf3945be5770b2660a152b7 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 b171aa1e5c28b47827f9148c2a5fc6948fb22938d3da21e6551b12432f492472 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_signatures 0 4a22aeda1d2e36fd390065dac1c07955e9f8942e7ba858435281863653f373ba e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_task 0 f397f2886049eb96b4bd3028d8108c1624e3959f2d428ca4687b0005144f3154 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_task 0 b4a88debacae6771a3c7e050fe0ed882c33e40faf80afd056e843a0e5095fe75 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 6014e2f18ba2d587f70d59fcbe62f64c38e8c556291006ae3dfd76ef7259315a e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 23b824f67984709f3c01dc7cc97c4c682b2afbaa5b63677a79ce56a1a58e6454 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 skipped 0 5b9749fdafec842ae75c6ad07e9eac8eb435653f8e5fc1ce452853fc36ff96f2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/receiptpostcheck.sh b/test/receiptpostcheck.sh index c445f613c..8ece1fd71 100755 --- a/test/receiptpostcheck.sh +++ b/test/receiptpostcheck.sh @@ -157,16 +157,26 @@ GOT_T="$( jq_field tests_to_run < "$TMP/r1.json" )" if [ "$GOT_T" = "__ABSENT__" ]; then no "(3) the receipt carries no tests_to_run — the second call the stderr hint asks for" else - python3 - "$TMP/r1.json" "$AFF" <<'PY' -import sys, json, re + ROOT="$ROOT" python3 - "$TMP/r1.json" "$AFF" <<'PY' +import sys, os, json, re +sys.path.insert(0, os.path.join(os.environ["ROOT"], "test")) +import testrowpaths # THE shared tests_to_run row reader rows = json.load(open(sys.argv[1]))["tests_to_run"] aff = sys.argv[2] -want = [] -for m in re.finditer(r'', aff): # F1: evidence attrs ride between p= and run= - want.append((m.group(1), m.group(2))) -got = [ (t["p"], t.get("run")) for t in rows ] +# E1 / review of #214: a row may name SEVERAL files, in either dialect — `` +# in the XML and a "p" ARRAY in the JSON. This comparison read the XML's single rows only and assumed the +# JSON's "p" was a string, so on a corpus where the rows group it compared a shorter list to a crashing +# one. Both sides are now read the same way, through the shared reader: the FILES each names, in order. +want = testrowpaths.xml_paths(aff) +got = testrowpaths.json_paths('{"tests_to_run":' + json.dumps(rows) + '}') assert got == want, "receipt tests_to_run %r != --affected rows %r" % (got, want) assert want, "the fixture reached no test file — the assertion would be vacuous" +# the run recipe still has to agree, per SINGLE row (a group row carries run_unknown by construction and +# has no per-path recipe to compare): key the XML singles by path and check the JSON's singles against them. +xrun = dict((m.group(1), m.group(2)) for m in re.finditer(r']*?(?: run="([^"]*)")?/>', aff)) +for t in rows: + if isinstance(t.get("p"), str) and t["p"] in xrun: + assert t.get("run") == xrun[t["p"]], "receipt run recipe for %s: %r != %r" % (t["p"], t.get("run"), xrun[t["p"]]) print("OK") PY [ $? -eq 0 ] \ diff --git a/test/rootrelemitcheck.sh b/test/rootrelemitcheck.sh index 84e60283a..b13f0d032 100755 --- a/test/rootrelemitcheck.sh +++ b/test/rootrelemitcheck.sh @@ -341,10 +341,12 @@ done # diffed with `sort | uniq`. The arms above catch each verb's leak in isolation; this one pins the CROSS-VERB # property directly, because that is the property an agent actually consumes. Run at BOTH root spellings: a # verb can be self-consistent and still disagree with its siblings on only one of the two. -tests_to_run_rows(){ # → one path per line +# E1 / review of #214: a tests_to_run row may name SEVERAL files (``), and +# this reader saw only the singles — on a corpus where the rows group it compared two EMPTY lists and passed. +# Every dialect is now read by test/testrowpaths.py, THE shared reader. +tests_to_run_rows(){ # → one path per line, emitted order local root="$1"; shift - "$BIN" "$root" "$@" 2>/dev/null \ - | tr '<' '\n' | sed -n 's/^test p="\([^"]*\)".*/\1/p; s/^t p="\([^"]*\)".*/\1/p' + "$BIN" "$root" "$@" 2>/dev/null | python3 "$ROOT/test/testrowpaths.py" paths xml } # This arm needs a corpus that HAS a test file. $SHORT/$DEEP do not: the fixture's one cross-dir caller is a # test only by virtue of the "test/fixture/…" path it lives at in THIS repo, and a copy of it elsewhere is @@ -368,15 +370,15 @@ for spelling in abs rel; do # this corpus, reaches only the cross-dir consumer — the symbol reading is the one that reaches the test. a_rows=$( cd "$CD" && tests_to_run_rows "$RT" --affected=distance ) g_rows=$( cd "$CD" && tests_to_run_rows "$RT" --test-gate=geometry.cpp ) - # the JSON twin carries "p" in BOTH arrays — narrow to tests_to_run so this compares like with like + # the JSON twin carries "p" in BOTH arrays — the shared reader slices tests_to_run by BRACKET DEPTH (a + # group row's "p" is itself an array, so the first ']' is not the end of the list) and unwraps both shapes j_rows=$( cd "$CD" && "$BIN" "$RT" --test-gate=geometry.cpp --json 2>/dev/null \ - | sed -n 's/.*"tests_to_run":\[\([^]]*\)\].*/\1/p' | tr ',' '\n' | sed -n 's/.*"p":"\([^"]*\)".*/\1/p' ) - # M21(b) re-pin (capture-audit 2026-09-04, lane L8): every --situ tests-to-run line now ends in a run - # recipe OR its "(run: not derivable)" disclosure, so the old "the line contains no '(' " extraction - # matched nothing and this arm read red while the SPELLING it exists to compare was correct. Re-pinned to - # the new contract: take the path FIELD off a row line, not the whole line. + | python3 "$ROOT/test/testrowpaths.py" paths json ) + # M21(b) re-pin (capture-audit 2026-09-04, lane L8): every --situ tests-to-run line ends in a run recipe + # OR its "(run: not derivable)" disclosure. E1 then made a line able to carry SEVERAL paths, and `$1` of + # such a line is `[hops=1]`, not a path — so the text dialect goes through the shared reader too. s_rows=$( cd "$CD" && "$BIN" "$RT" --situ=geometry.cpp 2>/dev/null \ - | sed -n '/tests to run/,/^ \[3\]/p' | awk '/^ [^ (]/ { print $1 }' ) + | python3 "$ROOT/test/testrowpaths.py" paths text ) if [ -z "$a_rows" ]; then no "ARM6/$spelling --affected emitted NO test row for distance — the arm would be a false green" continue diff --git a/test/selectorchaincheck.sh b/test/selectorchaincheck.sh index 14c93aa43..ca8e2d21a 100755 --- a/test/selectorchaincheck.sh +++ b/test/selectorchaincheck.sh @@ -120,7 +120,9 @@ esac # ── (d) --affected accepts a pasted `path:line` locator ─────────────────────────────────────────────── D_L="$( run --affected=src/graph.h:1148 2>/dev/null )" D_P="$( run --affected=src/graph.h 2>/dev/null )" -tset(){ printf '%s' "$1" | grep -oE '`), so the set of files +# is read by test/testrowpaths.py — THE shared reader — not by a grep for one row shape. +tset(){ printf '%s' "$1" | python3 "$ROOT/test/testrowpaths.py" paths xml | sort | tr '\n' ' '; } { [ -n "$( tset "$D_P" )" ] && [ "$( tset "$D_L" )" = "$( tset "$D_P" )" ]; } \ && ok "(d) --affected=src/graph.h:1148 ≡ --affected=src/graph.h (same test set)" \ || no "(d) --affected path:line ≠ path (line=[$( tset "$D_L" )] plain=[$( tset "$D_P" )])" diff --git a/test/testgatecheck.sh b/test/testgatecheck.sh index de37e4e3d..179b64118 100755 --- a/test/testgatecheck.sh +++ b/test/testgatecheck.sh @@ -51,8 +51,14 @@ printf 'int user() { return uncovered(); }\n' > "$R/src/user.cpp" run(){ perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" "$@" --no-cache 2>/dev/null; } rc(){ perl -e 'alarm 15; exec @ARGV' "$BIN" "$R" "$@" --no-cache >/dev/null 2>&1; echo $?; } attr(){ printf '%s' "$1" | grep -oE "$2=\"[0-9]+\"" | head -1 | grep -oE '[0-9]+'; } -# basenames of the emitted test rows, sorted -tset(){ printf '%s' "$1" | grep -oE ' +# group row (testmap.h's E1 shape, emitted whenever two runner-less tests share their evidence) was skipped +# without a word. Measured on a two-runner-less-test fixture: this helper returned the EMPTY set where the +# shared reader returned both paths. Only the one-test fixture below kept the arms honest, which is a +# property of the fixture and not of the reader. +tset(){ printf '%s' "$1" | python3 "$ROOT/test/testrowpaths.py" paths xml | sed 's|.*/||' | sort | tr '\n' ','; } # names of the emitted untested rows, sorted uset(){ printf '%s' "$1" | grep -oE ' 2486 B; 2540 leaves ~54 B — the same # posture as every pin above. -if [ "$legend" -le 2720 ]; then - ok "(a) --test-gate legend is $legend B (<= 2540 B budget; total=$total payload=$payload)" +# RE-PINNED 2720 -> 2900 (2026-09-12, output-routing loop E1 / A4-2, owner call). ONE new FACT, in the SAME +# row-gated clause (testmap.h kRunHintLegendClause, so the zero-row report still pays nothing): +# +180 B the group row — 2+ runner-less rows with equal evidence attributes served as ONE row, n= how +# many, p= their paths in list order, every path verbatim. It is what lets the not-derivable +# disclosure be said once per GROUP instead of once per row (rocksdb, 127 rows: 126 +# `run_unknown="1"` -> 10, test-gate 13,242 -> 9,633 B) and legendcoveragecheck wants n= defined +# wherever a document carries it. +# Measured on this fixture: 2663 -> 2843 B. +# RE-PINNED 2900 -> 3000 (2026-09-13, review of #214). TWO facts a consumer of a row cannot do without, +# both in the same row-gated clause, so a zero-row report still pays nothing: +# +65 B a path holding ',' is NEVER grouped, so p= splits into exactly n= paths. The seam used to spell +# such a path , and say so here; every XML parser undoes that entity BEFORE a consumer splits +# on the delimiter, so the escape was a promise the format could not keep. Refusing to group the +# row is the only spelling that is right in all three dialects, and this sentence is what makes +# `split( p, "," )` a safe thing for a reader to write. +# +49 B a section's shown=/total= over these rows count test FILES, so a row is N of them. Same +# finding from the other side: --pack-task prints above 30-odd +# RENDERED rows, and the bundle legend's own "shown=rows kept" sentence flatly contradicted it. +# Said HERE rather than in that always-on bundle legend, which is charged against the ceiling it +# describes: unconditional it put packtaskcheck's 2000-token arm 5620 B over a 5428 B ceiling +# (measured), and a bundle with no section has no use for it. +# Measured on this fixture: 2843 -> 2957 B; 3000 leaves ~43 B — the same posture as every pin above. +if [ "$legend" -le 3000 ]; then + ok "(a) --test-gate legend is $legend B (<= 3000 B budget; total=$total payload=$payload)" else - no "(a) --test-gate legend is $legend B (> 2540 B budget) — the essay re-inflated" + no "(a) --test-gate legend is $legend B (> 3000 B budget) — the essay re-inflated" fi # (b) the honesty vocabulary + the §B12.5 cross-verb UNIT-collision anchors (test/testgatecheck.sh arm (g) diff --git a/test/testgatepagecheck.sh b/test/testgatepagecheck.sh index 867936a28..ff26b2db4 100755 --- a/test/testgatepagecheck.sh +++ b/test/testgatepagecheck.sh @@ -202,7 +202,9 @@ printf 'int lib0() { return 0; }\nint lib1() { return 1; }\nint lib2() { return printf '#include "../src/lib.cpp"\nint test_lib0() { return lib0(); }\n' > "$TG/test/lib0_test.cpp" printf '#include "../src/lib.cpp"\nint test_lib1() { return lib1(); }\n' > "$TG/test/lib1_test.cpp" D="$( run "$TG" --test-gate=src/lib.cpp )" -DTROWS="$( printf '%s' "$D" | grep -o ' row, so the emitted +# count is FILES: the single rows plus every row's n= — shown_tests= must still equal that number. +DTROWS="$( { printf '%s' "$D" | grep -o ']*/>' | grep -oE ' n="[0-9]+"' | grep -oE '[0-9]+'; } | awk '{ s += $1 } END { print s + 0 }' )" DSHOWN="$( attr "$D" shown_tests )"; DTOTAL="$( attr "$D" tests )"; DCAP="$( attr "$D" tests_capped )" DWANT=0; [ "${DSHOWN:-0}" -lt "${DTOTAL:-0}" ] && DWANT=1 { [ -n "$DSHOWN" ] && [ "$DSHOWN" = "$DTROWS" ] && [ "$DCAP" = "$DWANT" ]; } \ diff --git a/test/testrowpaths.py b/test/testrowpaths.py new file mode 100644 index 000000000..8b330c997 --- /dev/null +++ b/test/testrowpaths.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 +"""testrowpaths.py — THE tests_to_run row reader every gate shares. + +WHY THIS FILE EXISTS. Seven gates read the PATHS out of the tests_to_run row family — affectedcheck, +impactpartitioncheck, receiptpostcheck, rootrelemitcheck, selectorchaincheck, testgatecheck, testrowruncheck +— and each one had grown its own reader: `grep -oE '` in XML, a `"p"` +ARRAY in JSON, `[hops=2] (3): a, b, c` in the text dialect. The private readers did not fail; they went +QUIET or, worse, wrong: + + * `grep -oE '"tests_to_run":\\[[^]]*\\]'` stops at the first `]`, which is now the end of the FIRST + group's path array — three gate arms asserted over two and a half rows and passed vacuously; + * `sed -n 's/^test p=//'` and friends saw the singles and silently skipped every group row; + * the text reader took `$1` of the line, which on a group line is `[hops=1]`, not a path. + +The invariant all six actually want is THE FILES NAMED, in emitted order. That is one question, so it is +answered in one place, for every dialect, and a gate that adds a new assertion gets the group shapes for +free instead of re-deriving them. Three further gates read these rows and are NOT converted, because none +asks for the paths: test/listingpagingcheck.sh sums `n=` over the rows to prove the family never pages, +test/w3fixlegendcheck.sh counts path occurrences on a --situ line, and test/testgatepagecheck.sh adds the +singles to the groups' `n=` to check shown_tests=. All three were made group-aware in place (E1) and stay +that way — routing a COUNT through a path reader would only add a dialect hop. + +THE CLAIM ABOVE WAS ONCE HALF TRUE, WHICH IS WHY THE CENSUS IS RECORDED HERE. The review of #214 found TWO +path readers still private: affectedcheck's own tset() (in the file this docstring already named — it split +EVERY row's p= on ',', including a single row's, so a comma-bearing path became two names that name nothing) +and testgatecheck's tset() (matched ` pin, handoffcheck's +run= check). deeptailcheck's ` and singles, groups. Comments are stripped FIRST, so + the legend's own `` definition is never read as a row. + json every object inside the balanced "tests_to_run":[…] list whose "p" (or "test") key is a string or + an array of strings. The list is sliced by bracket depth, not by the first `]`. + text --situ's ` path [hops=N] (run: …)` singles and its + ` [hops=N] (n): a, b, c (run: not derivable)` group lines. + +A path is emitted VERBATIM, exactly as the document spelled it, except that XML entity references are +decoded (`&` `<` `>` `"` `&#NN;`) — a gate compares paths against the file names it created, +which are unescaped. Note that a path containing ',' is never grouped (testmap.h refuses to), so splitting +a group's p= on ',' cannot split a path in half. +""" + +import json +import re +import sys + +_ENT = { "amp": "&", "lt": "<", "gt": ">", "quot": '"', "apos": "'" } + + +def xml_unescape( s ): + def one( m ): + body = m.group( 1 ) + if body.startswith( "#" ): + try: + return chr( int( body[2:], 16 ) if body[1:2].lower() == "x" else int( body[1:], 10 ) ) + except ValueError: + return m.group( 0 ) + return _ENT.get( body, m.group( 0 ) ) + return re.sub( r"&([#0-9A-Za-z]+);", one, s ) + + +def strip_comments( doc ): + """Drop every run. The legends define the row shapes they describe, so a reader that keeps + comments reads the DEFINITION of as a row with the paths 'a', 'b' and 'c'.""" + return re.sub( r"", "", doc, flags = re.S ) + + +def xml_paths( doc ): + """The test FILES an XML document names, in emitted order. + + `` + with the same opener. A tests_to_run group is qualified by run_unknown="1", which every group row carries + by construction (a group exists only where no runner is derivable), so that is what this reader matches — + the same qualification the compact legend uses to keep the two readings of `` apart.""" + out = [] + for m in re.finditer( r'<(?:t|test|g)\b[^>]*?\bp="([^"]*)"[^>]*/>', strip_comments( doc ) ): + row = m.group( 0 ) + raw = m.group( 1 ) + if row.startswith( " gate row, not a test group + # a group's p= is a comma-separated list; a path containing ',' is never grouped (testmap.h + # refuses to), so splitting on ',' cannot split a path in half. + out.extend( xml_unescape( p ) for p in raw.split( "," ) if p != "" ) + else: + out.append( xml_unescape( raw ) ) + return out + + +class TestRowParseError( Exception ): + """A document that NAMES a tests_to_run list this reader cannot read to its end. + + CodeRabbit on #214: this was not distinguished from the field being absent. Both returned None, json_paths + turned None into [], and the caller got an empty answer and exit 0 — so a TRUNCATED document (a gate that + captured a killed run, a byte cap that cut mid-array) asserted over zero rows and passed vacuously. That is + the very failure this file was written to end. The two cases are not the same claim: no `tests_to_run` field + is an ANSWER ("this document serves no such list", None -> []), while a field whose array never closes is a + document this reader cannot read at all, and the only honest thing to return is an error. Exit 2, as the + module header promises for exactly this.""" + + +def json_list_slice( doc ): + """The "tests_to_run":[…] value, sliced by BRACKET DEPTH (a group row's "p" is itself an array, so the + first ']' is not the end of the list) and string-aware (a ']' inside a path is not a bracket). + + Returns None when the document has no "tests_to_run" field at all. Raises TestRowParseError when it has + one that this reader cannot slice — see that class for why the two are not the same answer. + + The value is read ADJACENTLY. `doc.find( "[", i )` was an unbounded forward scan, so a document spelling + `"tests_to_run":null` was not read as "not a list": the scan walked past the value and sliced the NEXT + '[' anywhere in the document. `{"tests_to_run":null,"other":[{"p":"ghost.cpp"}]}` returned ghost.cpp at + exit 0 — a foreign field's paths served as this field's answer, which is the hole of arm 16's own species + one step worse, because the caller is handed rows instead of silence. Past the key, a ':', optional + whitespace, and the very next character must be '['; anything else is the raise the docstring promised.""" + i = doc.find( '"tests_to_run"' ) + if i < 0: + return None + j = i + len( '"tests_to_run"' ) + while j < len( doc ) and doc[j].isspace(): # JSON allows whitespace on both sides of ':' + j += 1 + if j >= len( doc ) or doc[j] != ":": + raise TestRowParseError( '"tests_to_run" is present but is followed by no ":" — not a field at all' ) + j += 1 + while j < len( doc ) and doc[j].isspace(): + j += 1 + if j >= len( doc ) or doc[j] != "[": + got = doc[ j:j + 12 ] if j < len( doc ) else "end of document" + raise TestRowParseError( '"tests_to_run" is present but its value does not open with "[" — not a list ' + 'at all (value begins %r)' % got ) + i = j + depth = 0 + instr = False + esc = False + for k in range( i, len( doc ) ): + c = doc[k] + if instr: + if esc: + esc = False + elif c == "\\": + esc = True + elif c == '"': + instr = False + continue + if c == '"': + instr = True + elif c == "[": + depth += 1 + elif c == "]": + depth -= 1 + if depth == 0: + return doc[i:k + 1] + raise TestRowParseError( '"tests_to_run" list is never closed: %d bracket(s) still open at end of document ' + '(%d bytes read from the field)' % ( depth, len( doc ) - i ) ) + + +def json_paths( doc ): + sl = json_list_slice( doc ) + if sl is None: + return [] + rows = json.loads( sl ) + out = [] + for r in rows: + if not isinstance( r, dict ): + continue + p = r.get( "p", r.get( "test" ) ) + if isinstance( p, list ): + out.extend( str( x ) for x in p ) + elif isinstance( p, str ): + out.append( p ) + return out + + +# The text dialect's row grammar, spelled from the RENDERER rather than guessed at (testmap.h: the Text arm +# of testRowEvidence, and runSuffixTextDisclosed). A row is +# +# [ [changed]][ [partner]][ [hops=N]] (run: |not derivable) +# +# and a GROUP row replaces with " (n): a, b, c", its attrs leading. The three evidence +# attributes are optional, but they are emitted in THAT ORDER and no other, and the run suffix always opens +# with exactly three spaces. So the whole tail of a row is a closed, known shape. +# +# CodeRabbit on #214: the single-row reader took `(\S+)`, which stops at the first space — a test path +# holding a space was reported TRUNCATED, silently, as a path that does not exist. A generic bracket matcher +# would have the mirror-image bug (a path holding "[...]" would lose it), which is why this is pinned to the +# renderer's own sequence instead: everything before the known attribute tail is the path, spaces included. +# The one ambiguity left is the dialect's, not the reader's — text carries NO escaping, so a path containing +# the literal three-space "(run: " opener cannot be told from the suffix. XML and JSON are exact; a gate that +# needs a path that adversarial should assert in one of those. +_TEXT_RUN_OPEN = " (run: " +_TEXT_ATTR_TAIL = re.compile( r"(?: \[changed\])?(?: \[partner\])?(?: \[hops=\d+\])?$" ) +_TEXT_GROUP = re.compile( r"^\s*(?:\[[^\]]*\]\s*)*\((\d+)\):\s*(.*?)\s{2,}\(run: " ) + + +def text_paths( doc ): + out = [] + for line in doc.split( "\n" ): + if _TEXT_RUN_OPEN not in line or not line.rstrip( "\r" ).endswith( ")" ): + continue + g = _TEXT_GROUP.match( line ) + if g: + out.extend( p.strip() for p in g.group( 2 ).split( "," ) if p.strip() ) + continue + # a SINGLE row: cut the run suffix, then the known attribute tail; what is left of the indent is the + # path, verbatim — spaces and all. + head = line[ :line.index( _TEXT_RUN_OPEN ) ] + head = _TEXT_ATTR_TAIL.sub( "", head ) + path = head.strip() + if path: + out.append( path ) + return out + + +def main( argv ): + if len( argv ) < 2: + sys.stderr.write( __doc__ ) + return 2 + mode = argv[1] + if mode == "jsonlist": + src = argv[2] if len( argv ) > 2 else None + doc = open( src ).read() if src else sys.stdin.read() + try: # an unbalanced list is exit 2 here too, not "" + sl = json_list_slice( doc ) + except TestRowParseError as e: + sys.stderr.write( "testrowpaths: json dialect unreadable: %s\n" % e ) + return 2 + sys.stdout.write( sl if sl else "" ) + return 0 + if mode != "paths" or len( argv ) < 3: + sys.stderr.write( __doc__ ) + return 2 + dialect = argv[2] + src = argv[3] if len( argv ) > 3 else None + doc = open( src ).read() if src else sys.stdin.read() + try: + if dialect == "xml": + paths = xml_paths( doc ) + elif dialect == "json": + paths = json_paths( doc ) + elif dialect == "text": + paths = text_paths( doc ) + else: + sys.stderr.write( "testrowpaths: unknown dialect %r (xml|json|text)\n" % dialect ) + return 2 + except Exception as e: # a document this reader cannot parse at all + sys.stderr.write( "testrowpaths: %s dialect unreadable: %s\n" % ( dialect, e ) ) + return 2 + for p in paths: + sys.stdout.write( p + "\n" ) + return 0 + + +if __name__ == "__main__": + sys.exit( main( sys.argv ) ) diff --git a/test/testrowruncheck.sh b/test/testrowruncheck.sh index b32dff245..432c182e6 100755 --- a/test/testrowruncheck.sh +++ b/test/testrowruncheck.sh @@ -17,9 +17,17 @@ # `"run_unknown":true` / `(run: not derivable)` disclosure — never neither. Plus M21(b)'s second half: the # untested blast-radius `` rows carry `l=`, the line their sibling --flags --flip rows have always had. # +# E1 / A4-2 (2026-09-12, owner call): runner-less rows that share their per-row attributes are served as ONE +# `` row (JSON `{"p":[…],…,"n":N,"run_unknown":true}`, text +# `[hops=N] (n): a, b, c (run: not derivable)`), so the disclosure is said once per GROUP. The rule keeps +# its meaning — "a or row carries one or the other, never neither" — and arm 12 proves what the +# grouping must never change: the MULTISET of paths (every path verbatim, each exactly once, across every +# dialect), on a fixture with three hop groups and a runner row in the middle of one of them. +# # ARM 0 is the DERIVATION arm: it enumerates the row emitters out of src/ and fails when a site appears that # the arms below do not drive. That is what makes this a family gate rather than seven instance gates — a -# NEW verb that grows a tests_to_run row is a FAILURE here until it is driven and disclosed. +# NEW verb that grows a tests_to_run row is a FAILURE here until it is driven and disclosed. Since E1 every +# emitter renders its rows through testmap.h's ONE seam (testRowsRendered), so the census is its call sites. # # Usage: test/testrowruncheck.sh # uses build/ripwire # RIPWIRE_BIN=asan/ripwire test/testrowruncheck.sh @@ -40,8 +48,8 @@ echo "testrowruncheck: BIN=$BIN" # ── ARM 0 — the emitter census, derived from src/ ────────────────────────────────────────────────────── # Every site that prints a tests_to_run row. The list is the CONTRACT: a site added to src/ and not added # here fails, which is the only way a family gate stays a family gate. -EXPECTED_SITES="src/verbs_change.h src/situ.h src/prcontext.h src/packtask.h src/handoff.h src/flipimpact.h src/mcpverbs.h" -FOUND_SITES="$( cd "$ROOT" && grep -lE '"<(t|test) p=\\"|\{\\"test\\":|\{\\"p\\":\\"%s\\"%s\}' src/*.h src/*.cpp 2>/dev/null \ +EXPECTED_SITES="src/verbs_change.h src/situ.h src/prcontext.h src/packtask.h src/handoff.h src/flipimpact.h src/mcpverbs.h src/mcpedit.h" +FOUND_SITES="$( cd "$ROOT" && grep -lE 'testRows(Rendered|Joined|List)\(|"<(t|test) p=\\"|\{\\"test\\":|\{\\"p\\":\\"%s\\"%s\}' src/*.h src/*.cpp 2>/dev/null \ | grep -vE 'src/(serialize|testmap)\.h' | sort | tr '\n' ' ' | sed 's/ $//' )" WANT_SITES="$( printf '%s\n' $EXPECTED_SITES | sort | tr '\n' ' ' | sed 's/ $//' )" [ "$FOUND_SITES" = "$WANT_SITES" ] \ @@ -105,6 +113,28 @@ cat > "$WORK/test/covered.sh" <<'EOF' echo covered EOF chmod +x "$WORK/test/covered.sh" +# a DARK gate whose guarded region sits in a file the tests reach, so --flags --flip has a report to make +# with rows in it (arm 7 skipped on every fixture for a release — it also read the wrong row element). +printf 'cmake_minimum_required( VERSION 3.20 )\nproject( trr )\noption( FEATURE_WRAP "the dark one" OFF )\n' > "$WORK/CMakeLists.txt" +# INSIDE wrapper's body, not beside it: a flip's tests are the tests that reach the gate's HOSTS, and a +# host that is itself dark is reached by nothing — the region has to sit in a function a test already calls. +cat > "$WORK/src/app.cpp" <<'APPEOF' +int compute( int x ) +{ + return x * 2; +} + +int wrapper( int x ) +{ +#ifdef FEATURE_WRAP + if( x > 100 ) + { + return compute( x + 1 ); + } +#endif + return compute( x ); +} +APPEOF ( cd "$WORK" && git init -q && git config user.email t@t && git config user.name t \ && git add -A && git commit -qm init >/dev/null 2>&1 ) # a second commit so the ref-taking verbs (--pr-context, --handoff) have a base to diff against, and a @@ -142,15 +172,25 @@ rows_disclosed(){ || printf ' NOTE %s listed no not-derivable row (verb reached only the covered harness)\n' "$label" } -XROW='<(t|test) p="[^"]*"[^>]*/>' +# E1: a group row is a tests_to_run row too (its p= lists several paths); the JSON twin's "p"/"test" +# is then an ARRAY. Both shapes must carry the disclosure like any single row. +XROW='<(t|test) p="[^"]*"[^>]*/>|]*/>' XHAS=' run="' XUNK=' run_unknown="1"' -JROW='\{"(p|test)":"[^"]*"[^}]*\}' +JROW='\{"(p|test)":("[^"]*"|\[[^]]*\])[^}]*\}' JHAS='"run":"' JUNK='"run_unknown":true' # the JSON dialects embed the tests_to_run LIST inside a document that also carries file rows keyed "p"; # slice the list first so the arm asks its question of the row family it is about and no other. -json_tests(){ printf '%s' "$1" | sed 's/\\"/"/g' | grep -oE '"tests_to_run":\[[^]]*\]'; } +# +# Review of #214: this slicer was `grep -oE '"tests_to_run":\[[^]]*\]'`, which stops at the first ']' — and +# since E1 the first ']' is the end of the FIRST GROUP's path array, not of the list. Arms 3, 5 and 9 were +# asserting over two and a half rows and passing vacuously. The slice is now taken by BRACKET DEPTH, in +# test/testrowpaths.py, which is also the reader every other gate in the tree uses for these rows. +ROWPATHS="$ROOT/test/testrowpaths.py" +json_tests(){ printf '%s' "$1" | sed 's/\\"/"/g' | python3 "$ROWPATHS" jsonlist; } +# the FILES a document names, in emitted order, in any dialect — singles and /array groups alike +row_paths(){ printf '%s' "$2" | python3 "$ROWPATHS" paths "$1"; } # ── ARM 1 — --affected (verbs_change.h) ──────────────────────────────────────────────────────────────── rows_disclosed "(1) --affected" "$( rw --affected=compute,wrapper )" "$XROW" "$XHAS" "$XUNK" @@ -168,7 +208,9 @@ rows_disclosed "(5) --pack-task json" "$( json_tests "$( rw --pack-task="change # ── ARM 6 — --handoff (handoff.h) — the row family lens 2 L2 found carrying NO run= at all ───────────── rows_disclosed "(6) --handoff" "$( rw --handoff )" "$XROW" "$XHAS" "$XUNK" # ── ARM 7 — --flags --flip (flipimpact.h) ────────────────────────────────────────────────────────────── -FLIPNAME="$( rw --flags | grep -oE '`; this arm read ` "$W2/src/leaf.cpp" +printf 'int leaf( int x );\nint mid( int x )\n{\n return leaf( x );\n}\n' > "$W2/src/mid.cpp" +printf 'int mid( int x );\nint top( int x )\n{\n return mid( x );\n}\n' > "$W2/src/top.cpp" +for n in leaf_a leaf_b leaf_c leaf_d; do printf 'int leaf( int x );\nint test_%s( void )\n{\n return leaf( 1 );\n}\n' "$n" > "$W2/test/t_$n.cpp"; done +for n in mid_a mid_b; do printf 'int mid( int x );\nint test_%s( void )\n{\n return mid( 1 );\n}\n' "$n" > "$W2/test/t_$n.cpp"; done +for n in top_a top_b; do printf 'int top( int x );\nint test_%s( void )\n{\n return top( 1 );\n}\n' "$n" > "$W2/test/t_$n.cpp"; done +printf '#!/usr/bin/env bash\necho leaf_b\n' > "$W2/test/t_leaf_b.sh"; chmod +x "$W2/test/t_leaf_b.sh" +( cd "$W2" && git init -q && git config user.email t@t && git config user.name t && git add -A && git commit -qm init >/dev/null 2>&1 ) +# a second commit and a dirty tree, so the DIFF-seeded verbs (--pr-context, --handoff) have a change set to +# answer about: without one they report files="0" and name no test at all, and arm 12's group-shape half +# (and arm 14's positive control) would be asserting over an empty document. +printf 'int leaf_extra( int x )\n{\n return x + 2;\n}\n' >> "$W2/src/leaf.cpp" +( cd "$W2" && git add -A && git commit -qm second >/dev/null 2>&1 ) +printf 'int leaf_extra2( int x )\n{\n return x + 3;\n}\n' >> "$W2/src/leaf.cpp" +rw2(){ ( cd "$W2" && "$BIN" . "$@" --no-cache 2>/dev/null ); } +A12="$( rw2 --affected=src/leaf.cpp )" +G12="$( rw2 --test-gate=src/leaf.cpp )" +J12="$( rw2 --test-gate=src/leaf.cpp --json )" +S12="$( rw2 --situ=src/leaf.cpp )" +P12="$( rw2 --pr-context )" +H12="$( rw2 --handoff )" +K12="$( rw2 --pack-task="change leaf" )" +KJ12="$( rw2 --pack-task="change leaf" --json )" +ROOT="$ROOT" python3 - "$A12" "$G12" "$J12" "$S12" "$P12" "$H12" "$K12" "$KJ12" <<'PY12' +import sys, os, re +sys.path.insert( 0, os.path.join( os.environ[ "ROOT" ], "test" ) ) +import testrowpaths as trp # THE shared reader — singles and /array groups, any dialect +aff, tg, tgj, situ, prc, hoff, pt, ptj = sys.argv[1:9] +ORDER = [ "test/t_%s.cpp" % n for n in ( "leaf_a", "leaf_b", "leaf_c", "leaf_d", "mid_a", "mid_b", "top_a", "top_b" ) ] # evidence order: hops asc, then path +EXPECT = sorted( ORDER ) +fails = [] + +# (a) the four dialects that serve the WHOLE list: same paths, same order, through the shared reader. +for label, doc, dialect in ( ( "--affected", aff, "xml" ), ( "--test-gate", tg, "xml" ), + ( "--test-gate --json", tgj, "json" ), ( "--situ text", situ, "text" ) ): + paths = trp.xml_paths( doc ) if dialect == "xml" else ( trp.json_paths( doc ) if dialect == "json" else trp.text_paths( doc ) ) + if sorted( paths ) != EXPECT: + fails.append( "%s multiset %r != %r" % ( label, sorted( paths ), EXPECT ) ) + if paths != ORDER: + fails.append( "%s ORDER changed by grouping: %r != %r (a group must cover a contiguous run only)" % ( label, paths, ORDER ) ) + +# (b) the group shape itself EXISTS, at three distinct hops=, and the run= row stayed a single row. +for label, doc in ( ( "--affected", aff ), ( "--test-gate", tg ) ): + body = trp.strip_comments( doc ) + hops = re.findall( r']*?\bhops="(\d+)"', body ) + if len( set( hops ) ) < 3: + fails.append( "%s: expected >=3 rows at distinct hops=, got hops=%r" % ( label, hops ) ) + runs = [ m.group( 0 ) for m in re.finditer( r'<(?:t|test|g)\b[^>]*?\brun="[^"]*"[^>]*/>', body ) ] + if len( runs ) != 1 or runs[0].startswith( " shapes of the OTHER emitters were produced by no arm at all. This fixture +# forms a group, so every one of them must show one — and every group row must carry the disclosure. +for label, doc, dialect in ( ( "--pr-context", prc, "xml" ), ( "--handoff", hoff, "xml" ), + ( "--pack-task", pt, "xml" ), ( "--pack-task --json", ptj, "json" ) ): + if dialect == "xml": + body = trp.strip_comments( doc ) + groups = re.findall( r"]*/>", body ) + if not groups: + fails.append( "%s renders no group row on a fixture that forms one" % label ) + for grow in groups: + if 'run_unknown="1"' not in grow: + fails.append( "%s group row carries no disclosure: %r" % ( label, grow ) ) + if not re.search( r'\bn="\d+"', grow ): + fails.append( "%s group row carries no n=: %r" % ( label, grow ) ) + else: + import json as _json + sl = trp.json_list_slice( doc ) + rows = _json.loads( sl ) if sl else [] + arrays = [ r for r in rows if isinstance( r.get( "p", r.get( "test" ) ), list ) ] + if not arrays: + fails.append( "%s renders no array (group) row on a fixture that forms one: %r" % ( label, rows ) ) + for r in arrays: + if r.get( "run_unknown" ) is not True or "n" not in r: + fails.append( "%s group row missing n= or the disclosure: %r" % ( label, r ) ) + # and the files a group names are files this fixture has + got = trp.xml_paths( doc ) if dialect == "xml" else trp.json_paths( doc ) + if not got: + fails.append( "%s named no test file at all" % label ) + for g in got: + if not g.endswith( tuple( os.path.basename( e ) for e in EXPECT ) ): + fails.append( "%s named a path this fixture does not have: %r" % ( label, g ) ) + +if fails: + print( "\n".join( fails ) ); sys.exit( 1 ) +print( "OK %d paths in order, groups present in 8 dialect/verb combinations" % len( EXPECT ) ) +PY12 +r12=$? +[ "$r12" -eq 0 ] \ + && ok "(12) E1: grouping keeps the path multiset AND order in every dialect, and every emitter renders the shape (8 paths, >=3 hop groups, the run= row single in place, tests= counts files)" \ + || no "(12) E1: the grouped rows do not carry the same paths, order or shape as the single rows did (details above)" + +# ── ARM 13 — E1: a path that ESCAPES WIDER than it reads never costs the section its rows ───────────── +# --pack-task's section is byte-budgeted. It used to GROUP first and hand the group rows to the +# generic list cutter under a per-row byte cap whose estimate was `attrs + 48 + Σ(path+1)` over UNESCAPED +# paths — so a corpus whose test paths hold '&' (or '<', or '"') rendered wider than the cap admitted, the +# cutter broke at the first over-budget entry, and the whole TAIL of the section went with it, run= singles +# included. The section now cuts over its own grouped, ESCAPED rendering (packTaskTestsSection). +# +# A true matched pair: the same ten test files, the same path LENGTHS, differing in exactly one byte per +# name — '&' in one fixture, '_' in the other — plus one harness with a runner so a run= single is in play. +# The property: at every budget the '&' fixture names at least one file whenever the control does (it may +# name fewer — escaped paths really are wider — but it must never collapse to nothing), and neither fixture +# ever goes backwards as the budget grows. Red on ff8d77a1: at --token-budget=1440 the control serves 5 +# files and the '&' fixture serves 0. +A13="$( mktemp -d )"; C13="$( mktemp -d )"; trap 'rm -rf "$WORK" "$W2" "$A13" "$C13"' EXIT +mk13(){ d="$1"; sep="$2"; mkdir -p "$d/src" "$d/test" + printf 'int compute_value( int x )\n{\n return x + 1;\n}\n' > "$d/src/core.cpp" + for n in a b c d e f x y z w; do printf 'int compute_value( int x );\nint test_%s( void )\n{\n return compute_value( 1 );\n}\n' "$n" > "$d/test/${n}${sep}t.cpp"; done + printf 'int compute_value( int x );\nint test_run( void )\n{\n return compute_value( 1 );\n}\n' > "$d/test/runme_t.cpp" + printf '#!/usr/bin/env bash\necho runme\n' > "$d/test/runme_t.sh"; chmod +x "$d/test/runme_t.sh" + ( cd "$d" && git init -q && git config user.email t@t && git config user.name t && git add -A && git commit -qm init >/dev/null 2>&1 ); } +mk13 "$A13" '&' +mk13 "$C13" '_' +named13(){ ( cd "$1" && "$BIN" . --no-cache --pack-task="compute_value" --token-budget="$2" 2>/dev/null ) | python3 "$ROWPATHS" paths xml | grep -c . ; } +bad13=""; prevA=0; prevC=0; sawA=0 +for b in 1440 1500 1560 1620 1680 1740 1800 1860; do + na="$( named13 "$A13" $b )"; nc="$( named13 "$C13" $b )" + [ "$na" -gt 0 ] && sawA=1 + [ "$nc" -gt 0 ] && [ "$na" -eq 0 ] && bad13="$bad13 budget=$b: control names $nc file(s), the '&' fixture names NONE" + [ "$na" -lt "$prevA" ] && bad13="$bad13 budget=$b: the '&' fixture went backwards ($prevA -> $na)" + [ "$nc" -lt "$prevC" ] && bad13="$bad13 budget=$b: the control went backwards ($prevC -> $nc)" + prevA=$na; prevC=$nc +done +if [ "$sawA" -ne 1 ] || [ "$prevC" -eq 0 ]; then + no "(13) neither fixture named a test file anywhere in 1440..1860 — the arm cannot bite" +elif [ -n "$bad13" ]; then + no "(13) a '&' in a test path costs the section its rows:$bad13" +else + ok "(13) E1: a path that escapes wider than it reads never drops the section (10 tests, '&' vs '_', budgets 1440..1860; both monotone, neither empty where the other is not)" +fi + +# ── ARM 14 — the run-hint clause is gated on ROWS at every site that splices it ─────────────────────── +# The clause is a rule ABOUT rows (~180 B). Review of #214: --handoff and --flags --flip spliced it +# unconditionally — and --handoff is BYTE-BUDGETED with heuristic rows dropped tail-first, so a packet with +# could evict a real row to pay for a rule about rows it has none of. Both now ask +# testmap.h's ONE gate (runHintClauseIfRows) with the count the seam returned. +# Fixture: a corpus with NO test file at all, so every verb below renders zero rows. Red on ff8d77a1 for +# --handoff; --flags --flip is asserted on the same corpus for the same reason. +N14="$( mktemp -d )"; trap 'rm -rf "$WORK" "$W2" "$A13" "$C13" "$N14"' EXIT +mkdir -p "$N14/src" +printf 'int alpha( int x )\n{\n return x + 1;\n}\n' > "$N14/src/a.cpp" +# a DARK preprocessor gate, so --flags --flip has something to report on this corpus too: its legend splices +# the same clause and, review of #214, spliced it unconditionally. +cat > "$N14/src/b.cpp" <<'B14' +int alpha( int x ); + +int beta( int x ) +{ + return alpha( x ) + 2; +} + +#ifdef FEATURE_ZETA +int zeta_only( int x ) +{ + return alpha( x ) * 3; +} +#endif +B14 +# the gate itself is a CMake option() — the shape --flags reports as kind="cmake" default="OFF" dark="1" +printf 'cmake_minimum_required( VERSION 3.20 )\nproject( n14 )\noption( FEATURE_ZETA "the dark one" OFF )\n' > "$N14/CMakeLists.txt" +( cd "$N14" && git init -q && git config user.email t@t && git config user.name t && git add -A && git commit -qm init >/dev/null 2>&1 ) +printf 'int gamma_fn( int x ) { return x - 1; }\n' >> "$N14/src/b.cpp" +CLAUSE='run= is the command that discharges a test row' +bad14="" +for v in --handoff --pr-context --test-gate --affected=src/a.cpp; do + o="$( cd "$N14" && "$BIN" . $v --no-cache 2>/dev/null )" + rows="$( printf '%s' "$o" | python3 "$ROWPATHS" paths xml | grep -c . )" + has="$( printf '%s' "$o" | grep -c "$CLAUSE" )" + [ "$rows" -eq 0 ] && [ "$has" -ne 0 ] && bad14="$bad14 $v(0 rows, clause present)" + [ "$rows" -gt 0 ] && [ "$has" -eq 0 ] && bad14="$bad14 $v($rows rows, clause MISSING)" +done +# --flags --flip on the same no-test corpus: zero rows, so no clause either +FN14="$( ( cd "$N14" && "$BIN" . --flags --no-cache 2>/dev/null ) | grep -oE '/dev/null )" + rows="$( printf '%s' "$o" | python3 "$ROWPATHS" paths xml | grep -c . )" + has="$( printf '%s' "$o" | grep -c "$CLAUSE" )" + [ "$rows" -eq 0 ] && [ "$has" -ne 0 ] && bad14="$bad14 --flags --flip=$FN14(0 rows, clause present)" + [ "$rows" -gt 0 ] && [ "$has" -eq 0 ] && bad14="$bad14 --flags --flip=$FN14($rows rows, clause MISSING)" +else + bad14="$bad14 --flags found no gate on the fixture (the flip half of this arm cannot bite)" +fi + +# the positive control: the grouping fixture from arm 12 DOES carry rows, so the same verbs must carry it +for v in --handoff --pr-context; do + o="$( rw2 $v )" + rows="$( printf '%s' "$o" | python3 "$ROWPATHS" paths xml | grep -c . )" + has="$( printf '%s' "$o" | grep -c "$CLAUSE" )" + [ "$rows" -gt 0 ] && [ "$has" -eq 0 ] && bad14="$bad14 control:$v($rows rows, clause MISSING)" + [ "$rows" -eq 0 ] && bad14="$bad14 control:$v named no row — the positive control cannot bite" +done +[ -z "$bad14" ] \ + && ok "(14) the run-hint clause rides exactly the documents that render a row (5 verbs on a no-test corpus, 2 positive controls)" \ + || no "(14) the run-hint clause is not rows-gated:$bad14" + +# ── ARM 15 — the partitioned bundle gates its outer clause on a COUNT, never on rendered bytes ──────── +# partition.h asked `xml.find( " CDATA +# quotes the literal text ``. Red on ff8d77a1. +Q15="$( mktemp -d )"; trap 'rm -rf "$WORK" "$W2" "$A13" "$C13" "$N14" "$Q15"' EXIT +mkdir -p "$Q15/src" +cat > "$Q15/src/emitter.cpp" <<'EOF' +#include + +void write_report( FILE* out, int n ) +{ + std::fprintf( out, "", n ); + std::fprintf( out, "" ); +} + +void caller_one( FILE* out ) +{ + write_report( out, 1 ); +} + +void caller_two( FILE* out ) +{ + write_report( out, 2 ); +} +EOF +printf 'void unrelated_helper( int x )\n{\n (void)x;\n}\n' > "$Q15/src/other.cpp" +( cd "$Q15" && git init -q && git config user.email t@t && git config user.name t && git add -A && git commit -qm init >/dev/null 2>&1 ) +O15="$( cd "$Q15" && "$BIN" . --no-cache --pack-task="write_report" --partition=2 2>/dev/null )" +lit15="$( printf '%s' "$O15" | grep -c ' section — it was meant to have no test file at all" +elif [ "$cls15" -ne 0 ]; then + no "(15) the partitioned bundle charges the run-hint clause for a body that merely QUOTES '/dev/null )"; a16rc=$? +[ "$a16rc" -eq 0 ] && [ -z "$a16" ] || r16bad="$r16bad [absent tests_to_run should be 0 paths at rc=0, got rc=$a16rc paths=$( printf '%s' "$a16" | tr '\n' ' ' )]" +t16="$( printf '%s' '{"tests_to_run":[{"p":["a","b"],"n":2},{"p":"c"}' | python3 "$ROWPATHS" paths json 2>/dev/null )"; t16rc=$? +[ "$t16rc" -eq 2 ] && [ -z "$t16" ] || r16bad="$r16bad [an unterminated tests_to_run array must be exit 2 with no rows, got rc=$t16rc paths=$( printf '%s' "$t16" | tr '\n' ' ' )]" +l16="$( printf '%s' '{"tests_to_run":[{"p":["a","b"],"n":2},{"p":"c"}]}' | python3 "$ROWPATHS" jsonlist 2>/dev/null )"; l16rc=$? +[ "$l16rc" -eq 0 ] && [ -n "$l16" ] || r16bad="$r16bad [the balanced control no longer slices: rc=$l16rc]" +j16="$( printf '%s' '{"tests_to_run":[{"p":["a","b"],"n":2},{"p":"c"}' | python3 "$ROWPATHS" jsonlist 2>/dev/null )"; j16rc=$? +[ "$j16rc" -eq 2 ] || r16bad="$r16bad [jsonlist swallows the same truncation: rc=$j16rc]" +# (b) the text dialect, in the renderer's own spelling (testmap.h: path, then [changed] [partner] [hops=N], +# then three spaces and the run suffix). Every path here holds a space; one holds all three attributes. +T16="$( printf '%s\n' \ + ' test/with space.cpp (run: not derivable)' \ + ' test/two words.cpp [hops=2] (run: ctest -R two)' \ + ' test/a b c.cpp [changed] [partner] [hops=1] (run: ctest -R abc)' \ + ' [hops=3] (2): test/g one.cpp, test/g two.cpp (run: not derivable)' )" +p16="$( printf '%s' "$T16" | python3 "$ROWPATHS" paths text 2>/dev/null | tr '\n' '|' )" +e16='test/with space.cpp|test/two words.cpp|test/a b c.cpp|test/g one.cpp|test/g two.cpp|' +[ "$p16" = "$e16" ] || r16bad="$r16bad [text single rows truncate a path at its first space: got '$p16' want '$e16']" +[ -z "$r16bad" ] \ + && ok "(16) the shared reader fails LOUDLY on a truncated tests_to_run array (exit 2, absent field still 0 rows) and keeps a text path's spaces" \ + || no "(16) the shared reader is still silent where it should fail:$r16bad" + + +# ── ARM 17 — THE THIRD SILENCE: "tests_to_run" PRESENT BUT NOT A LIST (review of #214) ──────────────── +# json_list_slice found the key and then ran `doc.find( "[", i )` — an UNBOUNDED forward scan. So a document +# spelling `"tests_to_run":null` was not read as "the field is not a list"; the scan walked PAST the value, +# found the NEXT '[' anywhere in the document, and sliced THAT. Measured on the reader as it stood at +# c9d6d4e8: `{"tests_to_run":null,"other":[{"p":"ghost.cpp"}]}` returned ghost.cpp at rc=0 — a foreign +# field's paths served as the tests_to_run answer, which is the same species of defect arm 16 closed and one +# step worse, because the caller is handed rows rather than silence. The docstring already promised the +# raise ("is followed by no '[' — not a list at all"); only the code disagreed. +# +# The fix reads the value ADJACENTLY: past the key, a ':', optional whitespace, and then the very next +# character must be '['. Every non-array value is exit 2; a well-formed array still parses, which is what +# the controls below hold. +r17bad="" +# (a) every non-array value raises — and the decoy '[' that used to be sliced names a path that would be +# served as a test row. null, a number, a string and an OBJECT all take this leg. +for d17 in '{"tests_to_run":null,"other":[]}' \ + '{"tests_to_run":null,"other":[{"p":"ghost.cpp"}]}' \ + '{"tests_to_run":7,"other":[{"p":"ghost.cpp"}]}' \ + '{"tests_to_run":"nope","other":[{"p":"ghost.cpp"}]}' \ + '{"tests_to_run":{"a":[{"p":"ghost.cpp"}]}}'; do + o17="$( printf '%s' "$d17" | python3 "$ROWPATHS" paths json 2>/dev/null )"; o17rc=$? + { [ "$o17rc" -eq 2 ] && [ -z "$o17" ]; } \ + || r17bad="$r17bad [$d17 -> rc=$o17rc paths='$( printf '%s' "$o17" | tr '\n' ' ' )', want rc=2 and no rows]" + # the jsonlist mode shares the slicer, so it owes the same answer + printf '%s' "$d17" | python3 "$ROWPATHS" jsonlist >/dev/null 2>&1 + [ $? -eq 2 ] || r17bad="$r17bad [jsonlist swallows the same non-list value: $d17]" +done +# (b) the CONTROLS: a well-formed array still parses, whitespace between ':' and '[' is legal JSON, and a +# group row's nested array is still sliced by depth rather than by the first ']'. +c17="$( printf '%s' '{"tests_to_run":[{"p":"ok.cpp"}]}' | python3 "$ROWPATHS" paths json 2>/dev/null )"; c17rc=$? +{ [ "$c17rc" -eq 0 ] && [ "$c17" = "ok.cpp" ]; } \ + || r17bad="$r17bad [the well-formed control no longer parses: rc=$c17rc paths='$c17']" +w17="$( printf '%s' '{"tests_to_run" : [{"p":["a","b"],"n":2},{"p":"c"}]}' | python3 "$ROWPATHS" paths json 2>/dev/null | tr '\n' '|' )"; w17rc=$? +{ [ "$w17rc" -eq 0 ] && [ "$w17" = "a|b|c|" ]; } \ + || r17bad="$r17bad [whitespace around the ':' must stay legal: rc=$w17rc paths='$w17']" +[ -z "$r17bad" ] \ + && ok "(17) a present-but-not-a-list tests_to_run is exit 2, never a slice of the NEXT field's array — the value is read adjacently" \ + || no "(17) the slicer still scans past its own field:$r17bad" + + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit "$fail" diff --git a/test/w3fixlegendcheck.sh b/test/w3fixlegendcheck.sh index e0699b659..7a0c14189 100755 --- a/test/w3fixlegendcheck.sh +++ b/test/w3fixlegendcheck.sh @@ -304,7 +304,9 @@ printf 'int coreFn(){ return 7; }\n' >"$SITSB/core.cpp" i=1; while [ $i -le 30 ]; do printf 'int coreFn();\nint t%02d_main(){ return coreFn(); }\n' "$i" >"$SITSB/test/t$i.cpp"; i=$(( i + 1 )); done "$BIN" "$SITSB" --no-cache --situ=core.cpp >"$TMP/situ30" 2>&1 S2LINE="$( grep -E '^ \[2\]' "$TMP/situ30" || true )" -S2ROWS="$( sed -n '/\[2\]/,/\[3\]/p' "$TMP/situ30" | grep -c 'test/t[0-9]*\.cpp' || true )" +# E1 (2026-09-12): runner-less rows sharing their evidence ride ONE `[hops=N] (n): a, b, …` line, so the +# count is of PATHS (occurrences), not lines — `grep -c` would count the 30 tests as 1. +S2ROWS="$( sed -n '/\[2\]/,/\[3\]/p' "$TMP/situ30" | grep -o 'test/t[0-9]*\.cpp' | wc -l | tr -d ' ' || true )" case "$S2LINE" in *"showing "*" of "*) no "situ [2] still discloses a cut on its ANSWER rows (the 25-row cap was retired): $S2LINE";; *"(30)"*) ok "situ [2]: '(30)' with no cut disclosed — answer rows never page";;