diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index c21b4c86..925aa09b 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -583,6 +583,7 @@ ack duplication c8b32435256b863e 33 by=src/* Phase 5 (docs/EVALS.md): the extern ack duplication cad2fb57e556cd72 40 audit pass 2026-08-06: per-cause as_* disclosure blocks in writeFieldAffinity follow the sibling one-if-per-attr pattern; chaseTypeCanPoint/isChaseRhsRow/looksLikePath are 2-call string predicates whose shared helper would be a worse abstraction; walks.cpp fixture twins are deliberate discriminating traps; short-horizon-churn is the audit editing just-landed code; runDefaultMap growth is the mapCtxOpenBytes fix + its measurement comment ack duplication caf81f56d57f97ec 53 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 ccc462865f2f2b59 40 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 +ack duplication ccdc625bb518d417 161 the merge-bomb threshold became the named constant kChurnMergeBombMaxFiles at all four call sites, as the review asked: a literal 100 sat beside a comment claiming parity with it. That one token is what pairs these two multi-root walks at 161 tokens - flipping it back clears the row, which is how this was verified. The bodies are not the same work: one accumulates a uint32 commit-frequency table from file SETS, the other sums a per-root double weight vector, so merging them would mean templating the ranking core on its accumulator to delete a similarity the constant created. ack duplication ce3ae91a3d24ac8c 64 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 cf6c83caaec68565 19 taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff ack duplication d02609215b4dde37 338 P3.2 plan-lint mechanical footprint: printUsage grows with the new help block; glyphName clones accessshape::shapeName (an enum-to-string switch idiom, the exact false-positive class P2.1's clone-idiom demotion targets, not yet landed); gitBlameLineSha clones quality.h::gitBlameRangeHasWindowCommit's blame-porcelain invocation shape — a shared-invocation extraction was tried and reverted as net-worse (it only moved the cost onto that untouched function's own churn), see planlint.h's own comment on the decision @@ -742,6 +743,7 @@ ack short-horizon-churn 0ab42b0dea75e0c1 4 cid=75fd6d9c66adca24 by=src/* Phase 5 ack short-horizon-churn 0ad109fca15e5792 3 cid=764c187d2f52c902 wave-3 close (--quality-delta=ec5e3c3..HEAD convergence): isShellBuiltinName takes externalnames.h's own house shape — a static_assert-sorted table read by binary_search/svLess, the one-liner its siblings isPythonBuiltin/isCFamilyStdName already are; the KindCounts::total match is token-shape only (accumulate over begin/end vs binary_search). The hand loop L7's P4 landed with cloned five unrelated predicates; this shape clones its two siblings, deliberately ack short-horizon-churn 0b848f97a57c94a8 2 cid=bed13ae0c0efec73 E2 (terminality round A, lane E): the blurb no longer coaches a redundant --edit-check after an edit (the receipt carries it) ack short-horizon-churn 0bd8141f1323059b 3 R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). +ack short-horizon-churn 0dce122a056e222c 84 cid=62f18627e6485f1d every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. | prior: writeRecentRows is the emitter of the new attribute; the second edit inside the churn window is the feature itself, not churn ack short-horizon-churn 0e08ebeb77eaf664 8 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 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. @@ -833,6 +835,7 @@ ack short-horizon-churn 39680772720129ac 3 cid=7419620b90eb7a8e by=src/* round-4 ack short-horizon-churn 39c462c8ccdc8d5e 6 cid=f9ceb035caa8adac 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 3a0425468e6a18cf 12 cid=76c5ec737574087c L10b finding 6: legend clause on --whereis/--doc-drift, conditional on --with-history | prior: T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack short-horizon-churn 3a54ea98a485670c 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 3b9db4b5e3202e61 79 cid=09b30efbb3ef0b77 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn 3ba0d3f31d319672 4 cid=e5a559aa3504947e 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 3be1c13661e5a63c 5 pack-task budget round (verifier K1+K2, 2026-08-19): +4 ccx / +14 LOC in packTaskBundleText is the section REORDER (bodies allocated last, after the four fine-grained prefix sections) plus the one-shot reflow lap's ranking branch; the three list-section top-ups were factored into reflowListSection rather than inlined, which is why the delta is this small. selectMonotoneBodySubset +1 ccx / +8 LOC is the one early return that admits the top-ranked candidate at every pool. churn=self on both is this change's own edit window. Measured payoff on this repo, --pack-task=rank the call graph: callers 13/20 to 20/20 and fill 52.5% to 59.8% at the default 6000-token budget, 43.5% to 90.6% at 8000; the task-named body now survives every budget increase. Both properties gated red-first in test/packtaskmonotoncheck.sh 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. @@ -846,6 +849,7 @@ ack short-horizon-churn 3eaf4cfffc6ac8b9 6 cid=7e40913eaabff86f rich-ingest prom ack short-horizon-churn 3ed0abc3c1d0ea4d 2 cid=9572a1a830ccca5d lane/xb-vendored-tier: filter.h is a hot file (>=2 commits/14d repo-wide); short-horizon-churn on isDemoOrGeneratedPath/kDemoOrGeneratedDirs is this edit's own self-churn, not a structural regression ack short-horizon-churn 3ffde9c631d4d539 5 cid=823d56445d19437d 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 402a3a53ef6e1227 8 cid=a7433af419f19a4c 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 409aac73eaf55d98 84 cid=d196d65f433fdf52 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn 41c6834a6145e687 16 cid=6769a7e38ab2ffb3 since git-sink hardening (test/sincecheck.sh S0-S4): resolveSinceScope refuses a leading '-' before any git call and resolves a revision through gitResolveCommitSha, and SinceScope drops revBoundary so git log is handed baselineSha, never the caller's string - the task edits exactly these two symbols (churn=self; N4 rewrote them 2026-09-04). The duplication row is the PRE-EXISTING twin prcontext.h::isCommitSha, re-keyed because isBareCommitSha/gitResolveCommitSha moved quality.h -> gitmine.h so resolveSinceScope could call them; consolidating prcontext.h/mergescout.h's resolver copies onto the shared pair is a follow-up lane, not this change ack short-horizon-churn 42ab5ed42c8d5187 5 cid=97f7b7bb44d49e47 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 431657fbc058d19b 5 cid=67f51a274892e20b OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -907,6 +911,7 @@ ack short-horizon-churn 6031be13b40a1b6f 37 cid=e77e1519a7e2bd9d lift-disclosure ack short-horizon-churn 60ab75751ccdbcae 37 cid=8c082954ad46aa28 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn 60c2747cfb13c22d 2 cid=547d8b0670517664 R1 identity round (harvest Wave-2 R1, 2026-08-24). Five gating rows, ONE change: finding identity now survives a rename or a move. (1) runQualityDelta complexity 201->205 and verbosity 419->462: the +43 lines are overwhelmingly the LEGEND this repo requires — the identity mechanism is a claim about what an ack means, so the report has to say what it rested on (both mechanisms, both truncation caps, the two per-route counters), and legendcoveragecheck arm (A) requires every new attribute be named there. The code added to this dispatcher is three lines: the ledger is now taken from the already-healed basis instead of re-read (re-reading it here would undo the healing), one countAckRescues call, and one call to quality::identityDisclosure — which is itself an EXTRACTION made in this lane precisely because --quality-delta flagged the inline version. The residual +4 complexity is the cid ternary at the ack-write site. Decomposing runQualityDelta is its own round, recorded as such by the W2-E ack above and unchanged by this one. (2) resolveDeltaBasis verbosity 73->86 + churn=self: the two healIdentity calls (ref-pair and working-tree arms) and the comment explaining why the healing must precede computeDelta — heal the baseline after the delta is taken and the delta has already been computed against the stale identity. churn=self on it and on mergescout::injectFileLevelFallback is this one edit window, not a history signal. (3) injectFileLevelFallback: its hand-rolled fopen/fseek/fread/fclose block was retired onto docparse::detail::readWholeFile, the canonical whole-file read, after this lane extracted quality.h's copy and the reuse-decline kind reported the pair as a 320-token clone. Re-read before the swap: short read or ftell failure clears the buffer and the file contributes nothing, byte-identical to what it replaced. Gate: test/identitycheck.sh, 19 arms, 9 RED on the pre-lane binary. ack short-horizon-churn 61d6cde8defa73ad 7 cid=5d0a21a4321d8300 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 61e3b36537394dcf 20 cid=0b2597fef4f733e3 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. 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 @@ -917,6 +922,7 @@ ack short-horizon-churn 6652d5114718eb63 7 cid=497a477af2561c0a OPTREMARKS F3 (d ack short-horizon-churn 66eacce77f5583fc 13 cid=36c336255a2b54d3 E2 (terminality round A, lane E): Outcome.next: the receipt's one follow-up, for the stderr line to repeat verbatim ack short-horizon-churn 673f75744ef30c37 5 cid=8d37d202da191bc1 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 685ade09a168535e 8 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean +ack short-horizon-churn 690e25213b20d02a 153 cid=7de3863dbaf7f712 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn 6979fa794f2a5154 13 cid=214a5f0c2c582530 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 69a315bf65e67852 5 cid=ba5aa20a7391f1b8 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 6b80d3e9ed466af2 5 cid=547ce2e0500da903 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. @@ -1044,6 +1050,7 @@ ack short-horizon-churn b32c5542781c4a86 44 W1-S2 churn-keying fix (pathQualifie ack short-horizon-churn b496a273ae1564ef 2 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn b4e161be7a843fd6 5 cid=2871d9a64ac68a01 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 b51ae02847c908a8 13 cid=b06b6b3d8c9bda0e 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 b574852c7fb75553 84 cid=ecae807f5737a5ba every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn b592c85cc907c27e 5 cid=55b6823c9ce621d8 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 b5cc5cd91ba8024b 6 cid=3fdbbea4c3225bdc 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 b792d6faac289d2e 154 cid=89cca6bb691a6095 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. @@ -1066,19 +1073,21 @@ ack short-horizon-churn c00b13f461715002 3 cid=0a077339a7a1e9fc F3 fix round (Fi ack short-horizon-churn c0d15239c2c4709d 24 cid=acf0020a7976fbe1 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn c0e0ed1f0514d6fe 10 cid=bfa2134f1e56ddf1 E1 (terminality round A, lane E): per-op seam disclosure (the plan receipt carries the same two keys as the single-edit receipt) ack short-horizon-churn c16d4b6e3f9eb4c8 6 cid=0184c1e2f4b48bc9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn c17db5239ed07cf7 95 cid=04f50efd091db7eb L10b finding 3: --recall --top-k=0 gets its own guard message (small, bounded verbosity growth in validateConfig's existing guard block) | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS +ack short-horizon-churn c17db5239ed07cf7 153 cid=e5f9535d40b86228 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. | prior: L10b finding 3: --recall --top-k=0 gets its own guard message (small, bounded verbosity growth in validateConfig's existing guard block) ack short-horizon-churn c29edcedb6d64b02 30 cid=21a776ff6c4e9bd6 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 c352bcfdeeeb51ac 79 cid=9afd690c35e996b9 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn c3f61f979f7e1278 10 cid=619c397b40760a9f E1 (terminality round A, lane E): the plan splices through the seam-aware applyEdit and keeps its SeamInfo per op ack short-horizon-churn c4d50b7393d16274 13 cid=f8caa0a79ac1b73e by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn c5726c667bc4ffa4 5 cid=21bba73836c9ef5a mention_files_capped read a scan STOP as a CUT: a false capped="1" at exactly kMentionMaxFiles matches with any later file, and on every mention after the list filled (one naming nothing, one re-naming a kept file by a longer path, one naming a symbol). The verdict now comes from what each mention NAMES, resolved the way an uncapped scan resolves it (mentionFilesCut -> namesFileNotKept; definesScopeName and namesUnkeptPackageIndex are its (b)/(c) routes). The two self-churn rows are liftPackageDirMention and the applyMentionBoost pass-1 loop going BACK to main shape with the stop-reason reads removed; the api-surface rows are those four helpers. Gated red-first by mentioncapcheck B4-B10; the lift itself is unchanged (mentioncheck ALL PASS). ack short-horizon-churn c5f1ad5fc3a12368 7 cid=c2b6e4d8e9e83369 lane/tc-sliceat MCP half: dispatchMcpLine +9cx/+21LOC is the per-verb dispatch cost every advertised tool pays (the var/flow/depth arg reads, the depth band static_assert, and the one slice branch); the verb logic itself lives in mcpverbs.h sliceText (new-symbol, mirrors the CLI runSlice refusal-for-refusal with sliceBundleText as the one shared emitter). Gate-covered red-first in test/mcpslicecheck.sh +ack short-horizon-churn c5f22701d0c88cb6 84 cid=937cf781fce3e33b every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn c769c8e4d28544b7 37 cid=32cb00692ba5fade lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: L10b finding 1: doc_mentions=/mention_anchored= root attrs on --for (small, bounded conditional growth) ack short-horizon-churn c840f9a6ec2e6c6b 26 W2-F consolidation pass: the same call sites the lane's first commit touched, re-touched to route through the single renderDisclosure() seam. churn=self reads THIS wave's second edit of each; no branch, parameter, symbol or nesting was added — the pass REMOVED four symbols and all four duplication findings ack short-horizon-churn c8702bea42b88461 5 cid=3ced8cf2ed4f9bec OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn c8c7bb0104aa16b8 44 W1-S2 churn-keying fix (pathQualifiedKey): bodyHashesBySym's pathQualified-param drop is the deliberate contract change (one keying, no mode); pathQualifiedKey is canonicalId/voteKey-SHAPED but a distinct key domain — canonicalId's bare-name degrade IS the bug this fixes, voteKey is a rename-vote pair with a different separator and value type; churn=self is this fix's own edit trail; gate: qualitysignalcheck.sh §1d ack short-horizon-churn c8f7a7244861651a 39 cid=3dc6618e7de489b6 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh ack short-horizon-churn c9ff98d0736199ea 3 finding #7 (2026-08-15 harvest): gitOnlyOmissionNote is the exact site the false 'not a git repository' claim lived at, so closing that finding necessarily edits this already-hot symbol again inside the short-horizon window; the edit is the fix, not incidental thrash -ack short-horizon-churn ca97a4b6bf07887b 32 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack short-horizon-churn ca97a4b6bf07887b 84 cid=5cc46613887f2c4a every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. | prior: R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack short-horizon-churn cb5f7d49e698fbaf 13 cid=1078c19d0b21d425 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn cb731eed086b4a0a 13 cid=401c7434ed757c00 by=src/* A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn cb7342964b38db9c 40 cid=c8da6499c2e07e45 at-seed MCP round: usesText's @-arm (the of= echo split + the atSeedNameOr rebind line) is this lane's own deliberate edit to a symbol that churned only in this same round; complexity/verbosity were already extracted into atSeedNameOr, churn=self is the edit itself @@ -1098,6 +1107,8 @@ ack short-horizon-churn d9e6c64c181ffc32 8 cid=5fb1e641981216da by=src/* round-4 ack short-horizon-churn d9e9626885a7219d 7 cid=25b10302481d18b0 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 da0f13e11b19d034 5 cid=7050dbe3657ff0c3 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 da1377ef5b455e83 7 cid=65e496283f09ee3a 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 da16ac00f586ff64 84 cid=5902ce56cd5f5b96 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. +ack short-horizon-churn da5738027a4ad28e 27 cid=c3a9e138ef4e8189 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn dbe6ed5269d328a4 21 cid=9c4a7dc830d23192 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 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 @@ -1121,7 +1132,7 @@ ack short-horizon-churn e6d92432fe4fd546 29 cid=b5e937b02b1a0718 by=src/* markdo ack short-horizon-churn e750b88b46822476 2 cid=50e93e1770abeaaf 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 e8abfc6e8faace3b 5 cid=ba58d27ecdba1f2f 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 ea9e556ae2c5b78f 3 cid=8f4414705d24d6f7 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 | prior: 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 ec2848a4801493a2 63 L7 lint-catalog: short-horizon-churn=self is ambient repo-wide churn on cli.h/main.cpp/didyoumean.h (49-92 commits/14d, unrelated lanes) inherited by any edit to these hot, central dispatch symbols, not fixable by reshaping this change; runLint/printUsage complexity+verbosity residual is the minimum new dispatch glue (4 extracted helper calls + 3 new flags' --help text) after extracting emitLintCatalog/resolveLintSelection/computeLintApplicability/printLintRuleTallyRow out of runLint, which cut the original complexity delta from +121 to +16 +ack short-horizon-churn ec2848a4801493a2 153 cid=e95af915562f1c12 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. | prior: L7 lint-catalog: short-horizon-churn=self is ambient repo-wide churn on cli.h/main.cpp/didyoumean.h (49-92 commits/14d, unrelated lanes) inherited by any edit to these hot, central dispatch symbols, not fixable by reshaping this change; runLint/printUsage complexity+verbosity residual is the minimum new dispatch glue (4 extracted helper calls + 3 new flags' --help text) after extracting emitLintCatalog/resolveLintSelection/computeLintApplicability/printLintRuleTallyRow out of runLint, which cut the original complexity delta from +121 to +16 ack short-horizon-churn ed0cbdfe7fa72e8c 154 cid=f42bf8683228ffff C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ed10de1685178c0e 12 cid=b144cdefb89bd164 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. ack short-horizon-churn ed4b3f43f7f19909 12 T3 disclosure-gap fix 2026-08-22: verbosity/churn on the two emitters + harness trace persistence are the registered disclosure's own bytes and comments; the gate-helper clone follows the self-contained-MCP-gate convention (every mcp gate carries its own mcp_call) @@ -1134,12 +1145,12 @@ ack short-horizon-churn f34bb3e60088fb42 5 cid=e63de9f5ee69d0f0 OPTREMARKS F3 (d ack short-horizon-churn f3642fd48bc3ef7d 7 short-horizon-churn on kCallHierarchyLegendOpen/runDefaultMap: both symbols sit in main.cpp/graphlegend.h, the two files carrying nearly the whole 2026-08-15 harvest wave (102 and 7 recent commits respectively per git log --since='2 weeks ago') — this fix's own edit is one more necessary touch during an active, intentional multi-lane round, not code churn from an unsettled design. Structurally quiet otherwise (no complexity/nesting/duplication regression on either symbol post-fix). ack short-horizon-churn f3a6e806f38c70a8 6 cid=085fdb748171c083 by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn f3bb371e01471bfb 5 cid=b58d650c34fb3728 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. +ack short-horizon-churn f3d7c8e9224298c2 9 cid=e7c339b781b86036 every short-horizon-churn row in this delta names a symbol this lane wrote or rewrote under review (the two compact-legend tables, the four map legends, MapAnnotations, serialize/writeRecentRows, pagingDisclosure, the cli.h guards, scopedRecentPage, refuseInertMainModifiers). The kind measures 2+ committed in-window rewrites of the same lines, which is what a reviewed lane does to its own code: round 1 added the block, round 2 answered CodeRabbit, round 3 answered the Fable review (the paging half, the stub vocabulary, the derived preemption, the crawl check). The churn here is the review cycle, not an unstable design, and it decays out of the window on its own. ack short-horizon-churn f3ea5dbe8b5720cb 6 cid=8b8d47057aed710b M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. ack short-horizon-churn 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 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 d78299c4..6f896583 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,226 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — `--in=DIR` scopes the recent-changes block to a directory and stubs the map it was not asked for + +"What changed recently in DIR?" is six of the thirty questions in this project's frozen reference set (the +RocksDB corpus pinned at `0e2801ac`, scored by the frozen-30 harness described in `docs/EVALS.md`), and +`--rank-by=churn-decay` answered it with a whole-repository symbol map plus one global `` +block that a directory with more than 40 recently-touched files never fits into; nothing in the binary took +a directory as a scope. `--in=DIR` (root-relative, an existing directory under the root; a trailing slash is +ignored, absolute paths and `..` are refused) keeps the global block byte-identical — three of the six golds +sit outside the named directory and complete only through it — and adds a second block +`` after it with DIR's files only, `p=` spelled root-relative exactly +as the global block spells them, same order. That spelling is the whole of the retrieval result: scored by +the same harness on the same 30 questions, the sub-root-relative spelling the obvious workaround produces +completed 0 of 30 against 19 of 30 for the root-prefixed one, because every gold path in the set is written +root-relative. + +The block pages the way every listing here pages, in `pageview.h`'s vocabulary: 40 rows by default, then +`capped=` beside its `n=` on every page (rule 3), `has_more=`/`next_offset=`/`offset=`/`limit=` when the +listing was cut or a window was asked for, and a pasteable `next=`. Its `of=` IS its total, so the paging +half carries no `total=`: one number under two names is a shape this project has removed elsewhere, because a +parser then has to know they are the same listing to avoid counting it twice. `next=` replays THIS +run's own corpus and window flags (`--since`, `--exclude`, `--no-ignore`, `--ignore-tests`), so the page it +names is a page of the same answer; a presentation flag is deliberately not replayed, because it cannot move +`of=`. Past 120 bytes (`kNextAttrMaxBytes`, the ceiling every other `next=` in the tool already respected) +the attribute is absent and `has_more=` still says the page exists — a hint that pastes wrong is worse than +none. The replayed set is ENUMERATED against the flags that can ride beside `--in`, not hand-picked: it also +carries `--max-file-size` when that run set one, because the crawl's size ceiling drops files out of the index +and the rows are indexed files (a hint emitted under `--max-file-size=2K` named a page of `of="3"` where the +run that emitted it saw `of="2"`), and `scopedMapNextInvocation` now lists every other rideable flag with the +reason it is NOT replayed — a cache flag indexes the same files, `--refetch` would fetch a newer tree, +`--scip` moves edges and not files, and the presentation flags cannot move `of=`. The scoped block rides +exactly when the global one does: an absent block means no history was mined, `n="0"` means history was mined +and no file under DIR was touched. That is a propagated FACT (`DecayedChurnMined::anyHistory`, carried through +`ChurnRanking` to the serializer) rather than a reading of the rows: a window whose only commit touched no +indexed file — the commit that deletes a file is the smallest one — has zero rows AND a mined history, and +inferring the second from the first published it as "no history was mined". + +The symbol map collapses to a disclosed stub `` — the map was +not asked for, so it is not ranked at all (no PageRank runs, and the header carries no `pr_iters=` for an +iteration that did not happen), and `would_show=` is that same run's own `shown=`: the symbol DEFINITIONS it +ranks, counted individually exactly as `shown=` counts them. The rows that run prints FOLLOW from the identity +the map legend already publishes for `shown=` — `rows + sum(overloads-1) = shown`, since the print loop +collapses a const/non-const overload pair into one row carrying `overloads="2"` (this repository: `shown="200"` +over 193 rows, 7 of them at `overloads="2"`). The first wording said "how many symbol ROWS", which is a number +the document does not contain. Reporting post-collapse rows there instead is not available at any price worth +paying: WHICH definitions survive the top-K cut is a fact about the ranking, and not ranking is the whole point +of the stub. It is named as the definition count it is rather than hedged as a ceiling, because a floor/ceiling +marker in this tool means "we could not see everything" — `counts_floor="1"`, `_capped`, the truncation +disclosures — while `would_show` is EXACT and only its UNIT differs from a reader's guess; spending an +uncertainty marker on a unit difference would make "ceiling" mean "exact, but not in the unit you assumed" and +weaken every honest use of the word elsewhere in the output. It deliberately borrows no paging attribute: `total=` is reserved for THE total (rule 2) and the stub's +number is a page size, `shown=` would drag a `capped=` with it (rule 3), and rule 3's own sentence sanctions +an element that carries neither. + +DIR is validated against the CRAWL and not only the filesystem — at least one indexed file must be spelled +`DIR/`, byte-exact. The filesystem answers a different question: on a case-folding volume `--in=DB` is a +directory, a symlink alias is a directory, and a subtree `--exclude` dropped is a directory, and all three +would otherwise be answered with an empty block, which reads as "nothing changed there". + +REFUSED, exactly, and `--help` lists the same set: with any flag that answers instead of the scoped map, +under multi-root, with `--top-k=N` for any N (the map it sizes is the stub), and with `--json`. The first of +those is DERIVED from the flag tables rather than from a list of verbs — the shape `--html` already used — +so `--map-diff`, `--expand`/`--outline`/`--pack-signatures`, `--doctor`, `--batch`, `--mcp` and the CLI edit +bridge are covered by the same three lines that cover a report verb, and a flag added tomorrow refuses +tomorrow with nobody editing the guard. `--in` is NOT a member of the paging verb set: it is a modifier of +the default map, and membership made the shaping guard refuse every `--top-k`/`--max-tokens`/`--token-budget` +beside it with a message naming verbs and claiming the default map honours the budgets it had just refused. +`--limit`/`--offset` compose (they window the scoped element) and so do `--max-tokens`/`--token-budget` (they +shape the document that is emitted). The MCP surface exposes no churn ranker, so there is no twin to extend. + +Two flags are refused for a DIFFERENT reason and now say so, and WHICH two is derived. `kMapShapingFlags` is +the tool's own list of flags that shape the bare map without selecting a verb, so that table minus the +ride-along table is exactly the residue a scoped run cannot compose with: `--no-redact`, `--metrics` and +`--map-diff`. `--map-diff` is the one that genuinely answers instead — it takes its own ranking branch ahead of +churn-decay, so no scoped block was ever going to be built — while the other two shape or un-redact a map this +run replaces with the counted stub. `--metrics` decorates symbol rows the stub does not print, and it is +refused as inert by table membership rather than by a hand-written case, so a shaping flag added tomorrow with +no ride-along row gets the right sentence without anyone editing the guard. `--no-redact` selects no +operation — it only stops +body redaction — and a scoped run serves no bodies at all, because the symbol map is the counted stub. So the +derived "answers instead" diagnostic stated a reason that was not this run's: the flag is INERT here, not +overridden. It is refused ahead of that diagnostic, in the shape the bare map already uses for the same flag, +pointing at both ways forward (drop it, or pass it to a body-serving verb). It is deliberately NOT added to the +ride-along table: accepting an inert modifier silently is the other half of the same defect. The class was then measured — and the first +measurement of it, published in this entry, was wrong. It said that of the flags the guard walks every one +probed reaches its own pairing refusal before this line, with `--external-surface` the only other flag +arriving here. Swept over the derived flag universe (`test/flaguniverse.py`) rather than a sample, 119 of the +171 `kBoolFlags`/`kViewFlags` rows reach that line, and 43 of those answer when run alone — so neither +"reaches the generic line" nor "answers alone" separates the class, because `--metrics` answers alone and what +it answers IS the default map, decorated. Table membership separates it, which is why the refusal derives the +set and `test/recentscopecheck.sh` arm 6s2e re-derives the same set from the same two tables on every run. +`--external-surface` remains a correct competitor and stays the control arm. The guard on the new branch is `--in` AND `--no-redact`: written without the first +half it refused every `--no-redact` run in the tool while quoting `--in=DIR` at it, which eight gates said in +one suite and no arm added for the fix could, since every one of them passes `--in`. + +Measured on the RocksDB corpus at `0e2801ac` (`--rank-by=churn-decay`, warm cache, bytes on stdout via +`wc -c`): 39,942 B bare → 10,601 B with `--in=db`, 10,525 B with `--in=util`, 11,071 B with `--in=table`. +The saving is the stub (69 B in place of the 200-row map); the scoped block itself costs ~2.3–2.8 KB per +answer, and the global block is unchanged. On this repository's own tree: 46,787 B → 9,629 B with +`--in=src`; on llvm-project (183,835 tracked files) 48,150 B → 6,051 B with `--in=llvm/lib/Analysis`. +Because the map is never ranked, sorted, bucketed or estimated under `--in`, the run is also cheaper, though +only by the share of it that ranking was: user time, median of five interleaved warm samples with a scratch +cache, 0.73 s → 0.71 s on RocksDB and 2.55 s → 2.37 s on llvm-project (~3% and ~7%). Ingest and the call +graph dominate both, and that is the honest size of this win. + +Gate: `test/recentscopecheck.sh`, 99 arms on a 53-commit fixture with 45 files under `db/` (plus two +fixtures of its own for the corpus arms) — the scoped rows +are only DIR's and spelled as the global block spells them, the global block is byte-identical with and +without the flag, page 2 (`--offset=40`) is the exact remainder with no overlap and the pasted `next=` +reproduces it byte-for-byte, `next=` replays `--exclude` and the pasted page lands on the same `of=`, an +over-120-byte `next=` is absent while `has_more="1"` remains, the stub carries no `total=`/`shown=`/`capped=` +and no `pr_iters=` rides the stubbed header, a window that mined nothing prints NEITHER block, a case-folded +name / a symlink alias / an excluded subtree each refuse naming the crawl, eight preemption arms sample the +derived refusal (`--lint`, `--hotspots`, `--query`, `--map-diff`, `--expand`, `--pack-signatures`, `--doctor`, +`--batch`), `--top-k` refuses with exactly one message where it used to print three, and `--max-tokens`/ +`--token-budget`/`--limit` compose with a clean stderr. Two arms cover the continuation's corpus directly: +a run under `--max-file-size=2K` on a fixture holding one oversize file must replay the ceiling and the pasted +page must land on the same `of=`, and a window whose only commit touched no indexed file must print `n="0"` +where `--since=HEAD` (which reads no commit) still prints neither block. `perl` and `xmllint` are +PREREQUISITES of the gate (exit 2, naming the tool) rather than arms: a missing tool is an environmental +condition, and reporting it as a FAIL made `test/regression.sh` name this gate as a product regression for a +tool the machine never had. Three arms cover the refusal wording: the inert `--no-redact` message, the +`--external-surface` control that must keep the competing wording, and — the one that was missing — a +`--no-redact` run with NO `--in` at all, which fails if the message so much as mentions the scoped flag. + +### Fixed — two generated documents published numbers and links nothing derived + +`docs/COMMANDS.md`'s table of contents is generated: one `[`--flag`](#anchor)` per entry, with the anchor +derived from the flag's spec. The derivation replaced every run of non-alphanumeric characters with a hyphen +and trimmed the ends (the `--in=DIR` heading became `#in-dir`), where the renderer's rule DELETES that +punctuation instead of substituting it — the anchor it mints keeps the flag's own two leading dashes and loses +the `=`. All 169 links in the document therefore resolved to nothing, and had done since it was first +generated; markdownlint's MD051 had been reporting it 28 times on a single line. Fixed in the +generator (`docs/docs_commands_build.py`), which now states the renderer's own rule — lower-case, drop every +character that is not a word character, a hyphen or a space, then spaces to hyphens — and assigns anchors in +emission order so a repeated heading would get the `-1` the renderer appends rather than two links to the +first. Gate: `test/docscommandscheck.sh` arm (J), which audits every fragment against the headings (fenced +sample output skipped — a `###` line inside a code block mints no anchor) and restates the renderer's rule +instead of importing the generator's, because a gate that asks the generator what the anchor should be agrees +with the generator's mistake. Arms (A)–(I) were all green throughout: (B) compares flag NAME sets and (G) +compares bytes, and a document can be byte-reproducible with every link in it dead. + +`docs/TUNING.md`, likewise generated, asserted a sum instead of deriving one: "`112 + 12` accounts for the 128 +NAMES" is 124, four short of the distinct-name count in the table two lines above it — in the one paragraph +whose subject is that quoting a wrong pair "would be wrong in both halves at once". Recounted from the same +data the table is built from: `src/` declares 129 caps under 128 distinct names, of which 111 are tunable, 12 +must stay `constexpr`, and 5 were declared after the sweep was prepared and no measurement has touched +(`kChurnMergeBombMaxFiles`, `kFieldIdCapacity`, `kForPageRowsDefault`, `kForPageUnionSymbolCap`, +`kMaxBlockBytes`) — 111 + 12 + 5 = 128. Neither 112 nor 12 was wrong: they are the FROZEN classification in +`bench/capsweep/tunable.tsv`, and the census beside them is re-read from `src/` on every run, so the two are +different populations and the missing four were five new names minus one (`kSituTestRowsShown`) the sweep +classified and `src/` no longer declares. The paragraph derives all three parts now, states that skew rather +than hiding it, and `capsweep.py emit` REFUSES to render a partition that does not add up — a name classified +in two lists at once, the shape an asserted sum cannot see, exits non-zero instead of publishing. Gate: +`test/capsweepcheck.sh` arm (C) reproduces the document byte-for-byte through that refusal on every run. + +### Fixed — a scoped run said its ranking fell back, having run no ranking + +`--rank-by=churn-decay` discloses a window that mined no commits: the teleport prior is uniform, so the map is +byte-identical to `--rank-by=pagerank`, and saying so is the difference between a degraded answer and a silent +one. Under `--in=DIR` that same sentence was false three ways at once, on a real invocation +(`--rank-by=churn-decay --in=src --since=HEAD`, a window that reads no commit). Nothing is ranked on a scoped +run — the rank vector is default-constructed and zero-filled, which is exactly why the header carries no +`pr_iters=` — so "using uniform (structural) ranking" named a computation that did not happen. "This map" +named a document the run does not contain, since the symbol map IS the counted stub and, with no history +mined, neither `` block rides at all. And the comparison it offered is unrunnable: `--rank-by=pagerank` +is refused beside `--in`, so the reader was pointed at a command the tool rejects. + +The scoped branch now states what did happen — no block rides, the map is the stub, nothing was ranked, so +there is no ranking to have fallen back — and keeps the pagerank equivalence where it is true, on the unscoped +run a reader gets by dropping `--in`. The two sibling callers (the multi-root arm and undecayed +`--rank-by=churn`) pass `stubbed=false` at the call site with the reason recorded: `--in` rides neither. +Gate: `test/recentscopecheck.sh` arm 13e asserts all three claims are gone and that the notice says nothing was +ranked, with 13f the control that the unscoped sentence survives unchanged. + +### Fixed — a pasteable `next=` quoted a tilde no shell expands + +Every `next=` in the tool is built by `nextFlag`, which quoted any value whose first character is `~` +whether or not a flag name preceded it. So a run under `--exclude=~tmp` published `--exclude='~tmp'`, and +because the attribute is XML the escaper rendered it `--exclude='~tmp'` — a replayed argument +corrupted to defend against an expansion that cannot happen. POSIX tilde expansion applies to a word whose +FIRST character is `~`; the word here is the whole argv element and it begins `--exclude=`, and an argument is +not an assignment. Measured on macOS, `sh`/`bash`/`zsh` alike: `sh -c 'p ~root'` passes `/var/root`, +`sh -c 'p --exclude=~root'` passes the literal `--exclude=~root`. The guard is now "word-initial AND no flag +name", a narrowing rather than a deletion — when the value IS the whole word (`src/editplan.h`'s rollback +invocation) the tilde really is word-initial and the quotes are load-bearing. + +The worse half was the gate. `test/nextverbcheck.sh` arm (9) pinned the entity-quoted form and explained it as +a shell expanding `~tmp`, which is wrong about POSIX twice over — the tilde is not word-initial there, and +`~tmp` expands nowhere anyway, since `~user` is expanded only for a user that exists. A gate that pins a false +belief does not merely miss the bug, it defends it against the next person to fix it, so the explanation is +deleted rather than reworded and the measured rule stated in its place. The arm pins the bare form, asserts the +invocation carries no `'` anywhere, and gains a mutation control: a space-bearing value is still quoted, +so the change narrowed the tilde case instead of disabling quoting. + +### Fixed — a churn window says how many commits it skipped as merge bombs + +The churn-decay miner behind `--rank-by=churn-decay` skips any commit touching more than 100 indexed +files — bulk renames, reformats, wide merges — and counted nothing about it, so a `` block +could silently omit the very commit a question was about: a held-out gold commit that touched 71 +source files (more than 100 in all) was invisible to the block, and nothing in the output said a commit +had been dropped. The block now carries `merge_bombs_skipped="N"` on every run, `"0"` included, so its +absence is never ambiguous; the full and compact legends define it and state the 100-file threshold +(`kChurnMergeBombMaxFiles`, now a named constant in `src/gitmine.h`, listed in `docs/LIMITS.md`). On this +repository's own tree the attribute reads `merge_bombs_skipped="5"` — five commits the map had been +quietly built without. Gate: `test/churndecaycheck.sh` arm 7 builds a repository whose HEAD commit adds +101 files and asserts the block reads `"1"`, that none of those files is a row, and that both legends +define the attribute; red on the previous binary (no attribute anywhere), green now. Both legends say +"more than 100 INDEXED files" — the rule counts the files this crawl HOLDS, never the commit's raw file +count, so a commit of 120 `.txt` files and one `.py` is not skipped and the old wording described a +different rule from the code's. The threshold is the named constant at all four call sites now; the two +`--rank-by=churn` walks kept a literal `100` beside a comment claiming parity with it. `merge_bombs_skipped=` +rides the GLOBAL block only: it counts the window's skipped commits, and stamping that number on a +directory-scoped element read as "N commits under DIR were skipped", which is wrong for any DIR smaller than +the repository. STILL UNDISCLOSED, and named here rather than left silent: `--cochange`, `--situ`'s co-change +partners and `--pr-context` apply their own commit-size skip at a cap of 30 with no counter at all — the same +class of silent drop, on three other verbs; disclosing those is a separate round. A window whose +every commit was skipped — a shallow clone of a large tree is exactly this shape: llvm-project at depth +1 is one 183,835-file commit — used to print no block at all, which reads as "no history mined"; it now +prints ``, zero rows and the reason (arm 7h, red +on the previous binary). A tree with no git history still prints no block. ### Fixed — `--expand` chose its serving mode on two different price lists `--expand`'s cheapest-complete-answer serving compares the default bundle against the whole file(s) the diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28169ba3..2dac4a66 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -520,11 +520,26 @@ Release CI job covered it. **The gate count is a build product.** It is stated in `README.md`, `docs/EVALS.md` and `present/deck5_ripwire_build.js` — eight sites — and every one of them is written by `docs/gatecount_build.py` from the single absorb loop in `test/regression.sh`, then gated by -`test/gatecountcheck.sh`. Hand-writing it is not a style preference: two lanes that each add one gate -both write N+1, git auto-merges the **identical** text clean, and the tree publishes N+1 against a loop -of N+2 with every existing check green (each branch's count matches its own loop, and the merged loop -matches main's — the member *sets* differ at the same number). That collided seven times in one night -on 2026-09-10. The merge recipe is therefore: **union the `for _g in …` sets, run the generator, done.** +`test/gatecountcheck.sh`. Hand-writing it is not a style preference. Two lanes that each add one gate +both write N+1, and git auto-merges that **identical** text clean in all three files — only the +`for _g in …` loop conflicts, so the loop is the only place anyone is forced to look. That collided +seven times in one night on 2026-09-10. The merge recipe is therefore: **union the `for _g in …` sets, +run the generator (no `--check`, so it WRITES), then `--check` it. Never hand-write the number and +never trust the clean auto-merge of the three published files.** + +WHAT CATCHES A BOTCHED RESOLUTION, measured on this tree 2026-09-13 rather than assumed, because the two +ways to botch it are caught by *different* gates and neither is caught by both: + +| botched how | the tree then has | red on | +| --- | --- | --- | +| loop unioned, generator not re-run | loop N+2, the eight sites N+1 | `gatecountcheck` (B), `manifestcheck`, `readmedriftcheck` (F2) — `deckclaimcheck` passes, but the deck's three sites are three of the eight `gatecountcheck` owns | +| one side of the loop taken instead of the union | loop N+1 and self-consistent, but a gate FILE present that the loop never names | `manifestcheck` only (`gatecountcheck` passes: the count really is consistent, and the generator has nothing to say) | + +So the count is fail-closed **provided the full suite runs** — which is why the suite before every push +is not negotiable. An earlier revision of this paragraph claimed the first row went green on every +existing check; that was true when it was written and is not true now, and a stale claim that a defect +is ungated costs more than the defect, because it sends people to build process around something three +gates already cover. **An advertised count is an enumeration, not a sentence.** Every number this project prints about itself — flags, gates, skills, folded repositories, orchestrator prompts — is derived from something diff --git a/README.md b/README.md index 543e10b8..229181a9 100644 --- a/README.md +++ b/README.md @@ -812,9 +812,9 @@ Full retrieval tables — including the MRR figures behind the router numbers ab ## What it answers
-179 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin +180 long flags across seven families, plus the MCP server — and --help-task names the ONE command a task wants, or abstains honestly when the evidence is too thin -Around the core sit 179 long flags advertised in `--help`, across seven families — plus an MCP +Around the core sit 180 long flags advertised in `--help`, across seven families — plus an MCP server, so a coding agent can call any of them mid-task instead of grepping and reading whole files. `--help` prints one line per flag (~4.5K tokens); `--help=--FLAG` prints that flag's full entry with every caveat, `--help=SECTION` one family, and `--help=all` the whole catalog. @@ -1204,7 +1204,7 @@ trailing `…`.
---callers — a call graph built on the spot, and why count="6" ships labelled a floor +--callers — a call graph built on the spot, and why count="7" ships labelled a floor **Ten seconds, no index server, no embeddings, no API key** — a parse and a call graph, built on the spot. The rows below are a real capture: the callers and their files are gate-held current @@ -1213,13 +1213,14 @@ grow — nothing can keep a line number true in a document, so it is not claimed ``` $ ripwire . --callers=rankGraphTeleport - - - - - - - + + + + + + + + ``` @@ -1227,7 +1228,7 @@ $ ripwire . --callers=rankGraphTeleport dispatch contributes no edge (a call through a function pointer or callback is an edge only when ONE function is bound to that variable in scope and the variable never escapes — its address taken or reference-bound — and a macro-generated call site — tagged -`role="macro"` — only when its function-like `#define` is indexed): `count="6"` is a **floor**, +`role="macro"` — only when its function-like `#define` is indexed): `count="7"` is a **floor**, and the element says so before you read a single row.
@@ -1804,9 +1805,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-615 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +616 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **615 gate scripts** and is the authoritative list; +`test/regression.sh` names **616 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2355,7 +2356,7 @@ identity of the index, and says which one is at fault. ### 5. Command families -The `--help` output groups 179 long flags advertised in `--help` into seven families. The `--help=` +The `--help` output groups 180 long flags advertised in `--help` into seven families. The `--help=` column below is the argument that prints one family: `ripwire --help=navigate`. `ripwire --help=--FLAG` prints one flag's full text — the caveats, the units, what it refuses and why. `ripwire --help=all` is the whole catalog, about 46,000 tokens. @@ -2398,12 +2399,13 @@ ripwire . --callers=rankGraphTeleport The leading legend comment is elided here; the line numbers are a capture and move as files grow: ```xml - + - - + + + ``` @@ -2541,7 +2543,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 210 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 211 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index 77115af0..8d9c1ba7 100644 --- a/bench/capsweep/capsweep.py +++ b/bench/capsweep/capsweep.py @@ -65,7 +65,7 @@ bench/capsweep/sweep.json into the answer to a query about a cap. assert_corpus_clean below keeps the harness out of the frozen CORPUS; the file format keeps it out of the INDEX. Same rule, two surfaces. """ -import argparse, hashlib, os, pathlib, re, shlex, shutil, subprocess, sys, collections +import argparse, hashlib, os, pathlib, re, shlex, shutil, subprocess, sys, textwrap, collections HERE = pathlib.Path(__file__).resolve().parent REPO = HERE.parent.parent @@ -827,6 +827,61 @@ def cmd_sweep(a): kRowsPerCap = 12 # rows per cap table; a cap in a document ABOUT caps, so it discloses below +def cap_partition(names, meta): + """The 128 distinct cap names, split three ways, with the sum CHECKED rather than claimed. + + CodeRabbit #212. The published summary ASSERTED that tunable + constexpr-only accounted for the + distinct names, and it did not: tunable.tsv's classification is FROZEN at the commit under + Provenance while the census is re-read from `src/` on every run, so a cap declared since the sweep + was prepared is in neither list, and a classified name `src/` has since dropped is in one of them + without being a live name at all. `112 + 12 = 124` shipped beside "128 distinct names" — four + short, in the one paragraph whose subject is that a wrong pair is wrong in both halves at once. + + So the three parts are derived from the same data the table is built from and the partition refuses + to render unless it adds up: a name classified in BOTH lists is exactly the shape an asserted sum + cannot see, and it exits non-zero here instead of being published. `gone` is the fourth fact — the + frozen classification's own residue — reported, never folded into the three. + """ + tun, ce = meta['tunable'], meta['constexpr_only'] + liveTun = [n for n in names if n in tun] + liveCe = [n for n in names if n in ce] + unswept = [n for n in names if n not in tun and n not in ce] + gone = sorted((set(tun) | set(ce)) - set(names)) + if len(liveTun) + len(liveCe) + len(unswept) != len(names): + sys.exit('capsweep: the cap partition does not add up (%d tunable + %d constexpr-only + %d unswept ' + '!= %d distinct names) — refusing to emit a summary whose arithmetic is wrong' + % (len(liveTun), len(liveCe), len(unswept), len(names))) + return liveTun, liveCe, unswept, gone + +def partition_prose(caps, names, meta): + """The paragraph that states the partition, with every number read from the partition itself. + + WRAPPED, not hand-broken: every number in it is derived, so pinning the line breaks by hand would + make a rewrap a second place to get the paragraph wrong. + """ + liveTun, liveCe, unswept, gone = cap_partition(names, meta) + dup = sorted(n for n in names if sum(1 for c in caps if c[0] == n) > 1) + shown = ', '.join('`%s`' % n for n in unswept[:6]) + ('' if len(unswept) <= 6 else ', +%d more' % (len(unswept) - 6)) + frozen = len(meta['tunable']) + goneSay = ('' if not gone else + ' The `tunable` column above reads %d rather than %d because %s no longer declared in `src/` at all: %s.' + % (frozen, len(liveTun), + 'one name the sweep classified is' if len(gone) == 1 else '%d names the sweep classified are' % len(gone), + ', '.join('`%s`' % n for n in gone[:4]))) + para = ('The first two columns are not the same number, and the gap is not a rounding: `src/` holds ' + '**%d cap declarations** under **%d distinct names** (%s declared in more than one file). The sweep ' + 'patches by NAME, so its own population is NAMES and not declarations — and the two columns beside ' + 'those are frozen at the commit named under Provenance while this census is re-read from `src/` on ' + 'every run, which makes them a third population again. Enumerated over the %d names `src/` declares ' + 'today: **%d tunable**, **%d must stay `constexpr`**, and **%d declared since the sweep was prepared, ' + 'which no measurement has touched** (%s). %d + %d + %d = %d, and `emit` refuses to render a partition ' + 'that does not add up.%s Quoting "%d of %d" would be wrong in both halves at once, which is the shape ' + 'of error a generated table exists to prevent.' + % (len(caps), len(names), ', '.join('`%s`' % n for n in dup) or 'no name', len(names), + len(liveTun), len(liveCe), len(unswept), shown, + len(liveTun), len(liveCe), len(unswept), len(names), goneSay, frozen + 1, len(caps))) + return textwrap.fill(para, width=100) + def render(sweep, meta, caps, disc): """docs/TUNING.md as a PURE FUNCTION of (frozen measurements, live cap census). @@ -854,15 +909,7 @@ def render(sweep, meta, caps, disc): L.append('| --- | --- | --- | --- | --- | --- |') L.append('| %d | %d | %d | %d | **%d** | %d |\n' % (len(caps), len(names), len(tun), len(ce), len(sweep), len(tun) - len(sweep))) - dup = sorted(n for n in names if sum(1 for c in caps if c[0] == n) > 1) - L.append('The first two columns are not the same number, and the gap is not a rounding: `src/` holds') - L.append('**%d cap declarations** under **%d distinct names** (%s declared in more than one file). The' - % (len(caps), len(names), ', '.join('`%s`' % n for n in dup) or 'no name')) - L.append('sweep patches by NAME, so `%d + %d` accounts for the %d NAMES — not the %d declarations. Quoting' - % (len(tun), len(ce), len(names), len(caps))) - L.append('"%d of %d" would be wrong in both halves at once, which is exactly the shape of error a') - L.append('generated table exists to prevent.\n') - L[-2] = L[-2] % (len(tun) + 1, len(caps)) + L.append(partition_prose(caps, names, meta) + '\n') L.append('## Read this ratio before the tables\n') L.append('**%d of %d tunable caps move any invocation at all. %d move nothing measurable.** That is the' % (len(sweep), len(tun), len(tun) - len(sweep))) diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index a74a8f35..978647dd 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -38,19 +38,19 @@ Two limits apply to nearly everything here and are not repeated in every section ## Contents -**understand a codebase cold** — [`--top-k`](#top-k-n) · [`--max-tokens`](#max-tokens-n) · [`--token-budget`](#token-budget-n-k-m-g) · [`--help-task`](#help-task-task) · [`--for`](#for-task) · [`--signatures-only`](#signatures-only) · [`--auto-bodies`](#auto-bodies) · [`--no-route`](#no-route) · [`--adaptive`](#adaptive) · [`--no-mention-boost`](#no-mention-boost) · [`--no-doc-mention`](#no-doc-mention) · [`--lego`](#lego-type) · [`--exemplar`](#exemplar-task-kind) · [`--recall`](#recall-task) · [`--tree`](#tree) · [`--html`](#html-file) · [`--color-by`](#color-by-mode) · [`--order`](#order-mode) · [`--no-stable`](#no-stable) +**understand a codebase cold** — [`--top-k`](#--top-kn) · [`--max-tokens`](#--max-tokensn) · [`--token-budget`](#--token-budgetnkmg) · [`--help-task`](#--help-tasktask) · [`--for`](#--fortask) · [`--signatures-only`](#--signatures-only) · [`--auto-bodies`](#--auto-bodies) · [`--no-route`](#--no-route) · [`--adaptive`](#--adaptive) · [`--no-mention-boost`](#--no-mention-boost) · [`--no-doc-mention`](#--no-doc-mention) · [`--lego`](#--legotype) · [`--exemplar`](#--exemplartaskkind) · [`--recall`](#--recalltask) · [`--tree`](#--tree) · [`--html`](#--htmlfile) · [`--color-by`](#--color-bymode) · [`--order`](#--ordermode) · [`--no-stable`](#--no-stable) -**navigate / answer a question** — [`--around`](#around-sym) · [`--callers`](#callers-sym) · [`--callees`](#callees-sym) · [`--uses`](#uses-sym) · [`--graph-query`](#graph-query-expr) · [`--external-surface`](#external-surface) · [`--path`](#path-src-dst) · [`--connect`](#connect-a-b-c) · [`--impact`](#impact-sym) · [`--verify`](#verify-claim) · [`--mentions`](#mentions-sym) · [`--affected`](#affected-f1-f2-sym) · [`--exercises`](#exercises-testfile) · [`--situ`](#situ-f1-f2) · [`--handoff`](#handoff) · [`--test-gate`](#test-gate-f1-f2) · [`--grep`](#grep-str-regex-pat) · [`--grep-context`](#grep-context-n-grep-before-n-grep-after-n) · [`--and`](#and-str) · [`--not`](#not-str) · [`--grep-scope`](#grep-scope-line-file) · [`--grep-in`](#grep-in-code-any) · [`--handles`](#handles) · [`--match`](#match-query) · [`--pattern`](#pattern-pat) · [`--query`](#query-terms) +**navigate / answer a question** — [`--around`](#--aroundsym) · [`--callers`](#--callerssym) · [`--callees`](#--calleessym) · [`--uses`](#--usessym) · [`--graph-query`](#--graph-queryexpr) · [`--external-surface`](#--external-surface) · [`--path`](#--pathsrcdst) · [`--connect`](#--connectabc) · [`--impact`](#--impactsym) · [`--verify`](#--verifyclaim) · [`--mentions`](#--mentionssym) · [`--affected`](#--affectedf1f2sym) · [`--exercises`](#--exercisestestfile) · [`--situ`](#--situf1f2) · [`--handoff`](#--handoff) · [`--test-gate`](#--test-gatef1f2) · [`--grep`](#--grepstr----regexpat) · [`--grep-context`](#--grep-contextn----grep-beforen----grep-aftern) · [`--and`](#--andstr) · [`--not`](#--notstr) · [`--grep-scope`](#--grep-scopelinefile) · [`--grep-in`](#--grep-incodeany) · [`--handles`](#--handles) · [`--match`](#--matchquery) · [`--pattern`](#--patternpat) · [`--query`](#--queryterms) -**zoom the detail ladder** — [`--detail`](#detail-n) · [`--pack-signatures`](#pack-signatures) · [`--outline`](#outline-a-b) · [`--expand`](#expand-a-b) · [`--compress`](#compress) · [`--pack-top-n`](#pack-top-n-n) · [`--no-redact`](#no-redact) +**zoom the detail ladder** — [`--detail`](#--detailn) · [`--pack-signatures`](#--pack-signatures) · [`--outline`](#--outlineab) · [`--expand`](#--expandab) · [`--compress`](#--compress) · [`--pack-top-n`](#--pack-top-nn) · [`--no-redact`](#--no-redact) -**assess quality / structure** — [`--metrics`](#metrics) · [`--deps`](#deps) · [`--hotspots`](#hotspots) · [`--clones`](#clones) · [`--readability`](#readability) · [`--nonlocal-state`](#nonlocal-state) · [`--ensemble`](#ensemble) · [`--quality-panel`](#quality-panel-preset) · [`--context-ratio`](#context-ratio) · [`--naming-calibration`](#naming-calibration) · [`--naming-consistency`](#naming-consistency) · [`--naming-locals`](#naming-locals) · [`--comment-coherence`](#comment-coherence) · [`--cochange`](#cochange-file) · [`--cochange-recur`](#cochange-recur-k) · [`--cochange-groups`](#cochange-groups) · [`--since`](#since-rev-date) · [`--arch`](#arch-file) · [`--arch`](#arch-file-baseline) · [`--arch`](#arch-file-baseline-update) · [`--lint`](#lint) · [`--lint-catalog`](#lint-catalog) · [`--lint-rules`](#lint-rules-dir) · [`--lint-select`](#lint-select-prefix) · [`--lint-ignore`](#lint-ignore-prefix) · [`--sarif`](#sarif) · [`--with-profile`](#with-profile-file) · [`--communities`](#communities) · [`--community`](#community-id) · [`--zoom`](#zoom-depth) · [`--report`](#report) · [`--seams`](#seams) · [`--mermaid`](#mermaid) · [`--owners`](#owners-sym) · [`--dead-code`](#dead-code-dir) · [`--quality-baseline`](#quality-baseline) · [`--allow-dirty`](#allow-dirty) · [`--quality-delta`](#quality-delta) · [`--quality-delta`](#quality-delta-rev-a-b) · [`--dmm`](#dmm-rev-a-b) · [`--quality-ack`](#quality-ack-reason) · [`--ack-only`](#ack-only-substr-substr) · [`--scope`](#scope-glob-glob) · [`--edit-check`](#edit-check-sym) · [`--replace-symbol-body`](#replace-symbol-body-target) · [`--insert-before-symbol`](#insert-before-symbol-target) · [`--insert-after-symbol`](#insert-after-symbol-target) · [`--edit-payload`](#edit-payload-file) · [`--edit-target-file`](#edit-target-file-path) · [`--no-post-check`](#no-post-check) · [`--edit-plan`](#edit-plan-file) · [`--dry-run`](#dry-run-apply) · [`--safe-delete`](#safe-delete-sym) · [`--slice`](#slice-sym-var) · [`--slice-flow`](#slice-flow-back-fwd-both) · [`--slice-depth`](#slice-depth-n) · [`--at`](#at-file-line) · [`--pr-context`](#pr-context-baseref) · [`--merge-scout`](#merge-scout-ref-ref) · [`--plan-lanes`](#plan-lanes-n-task-goal) · [`--plan-lanes`](#plan-lanes-brief-file) · [`--stray-content`](#stray-content-substr) · [`--plan`](#plan) · [`--abi`](#abi) · [`--whereis`](#whereis-sym) · [`--flags`](#flags-substr) · [`--flip`](#flip-name) · [`--layout`](#layout-struct) · [`--field-affinity`](#field-affinity-struct) · [`--doc-drift`](#doc-drift-substr) · [`--doc-drift`](#doc-drift-gateability) · [`--with-history`](#with-history) · [`--plan-lint`](#plan-lint-file) · [`--from-trace`](#from-trace-file) · [`--run-trace`](#run-trace-cmd) · [`--run-timeout`](#run-timeout-seconds) · [`--note-add`](#note-add-target-text) · [`--notes`](#notes) · [`--pack-task`](#pack-task-task) · [`--partition`](#partition-n) · [`--with-graph`](#with-graph) · [`--export`](#export-cc-json-file) · [`--batch`](#batch-file) +**assess quality / structure** — [`--metrics`](#--metrics) · [`--deps`](#--deps) · [`--hotspots`](#--hotspots) · [`--clones`](#--clones) · [`--readability`](#--readability) · [`--nonlocal-state`](#--nonlocal-state) · [`--ensemble`](#--ensemble) · [`--quality-panel`](#--quality-panelpreset) · [`--context-ratio`](#--context-ratio) · [`--naming-calibration`](#--naming-calibration) · [`--naming-consistency`](#--naming-consistency) · [`--naming-locals`](#--naming-locals) · [`--comment-coherence`](#--comment-coherence) · [`--cochange`](#--cochangefile) · [`--cochange-recur`](#--cochange-recurk) · [`--cochange-groups`](#--cochange-groups) · [`--since`](#--sincerevdate) · [`--arch`](#--archfile) · [`--arch`](#--archfile---baseline) · [`--arch`](#--archfile---baseline-update) · [`--lint`](#--lint) · [`--lint-catalog`](#--lint-catalog) · [`--lint-rules`](#--lint-rulesdir) · [`--lint-select`](#--lint-selectprefix) · [`--lint-ignore`](#--lint-ignoreprefix) · [`--sarif`](#--sarif) · [`--with-profile`](#--with-profilefile) · [`--communities`](#--communities) · [`--community`](#--communityid) · [`--zoom`](#--zoomdepth) · [`--report`](#--report) · [`--seams`](#--seams) · [`--mermaid`](#--mermaid) · [`--owners`](#--ownerssym) · [`--dead-code`](#--dead-codedir) · [`--quality-baseline`](#--quality-baseline) · [`--allow-dirty`](#--allow-dirty) · [`--quality-delta`](#--quality-delta) · [`--quality-delta`](#--quality-deltarevab) · [`--dmm`](#--dmmrevab) · [`--quality-ack`](#--quality-ackreason) · [`--ack-only`](#--ack-onlysubstrsubstr) · [`--scope`](#--scopeglobglob) · [`--edit-check`](#--edit-checksym) · [`--replace-symbol-body`](#--replace-symbol-bodytarget) · [`--insert-before-symbol`](#--insert-before-symboltarget) · [`--insert-after-symbol`](#--insert-after-symboltarget) · [`--edit-payload`](#--edit-payloadfile-) · [`--edit-target-file`](#--edit-target-filepath) · [`--no-post-check`](#--no-post-check) · [`--edit-plan`](#--edit-planfile) · [`--dry-run`](#--dry-run----apply) · [`--safe-delete`](#--safe-deletesym) · [`--slice`](#--slicesymvar) · [`--slice-flow`](#--slice-flowbackfwdboth) · [`--slice-depth`](#--slice-depthn) · [`--at`](#--atfileline) · [`--pr-context`](#--pr-contextbaseref) · [`--merge-scout`](#--merge-scoutrefref) · [`--plan-lanes`](#--plan-lanesn---taskgoal) · [`--plan-lanes`](#--plan-lanes---brieffile) · [`--stray-content`](#--stray-contentsubstr) · [`--plan`](#--plan) · [`--abi`](#--abi) · [`--whereis`](#--whereissym) · [`--flags`](#--flagssubstr) · [`--flip`](#--flipname) · [`--layout`](#--layoutstruct) · [`--field-affinity`](#--field-affinitystruct) · [`--doc-drift`](#--doc-driftsubstr) · [`--doc-drift`](#--doc-drift---gateability) · [`--with-history`](#--with-history) · [`--plan-lint`](#--plan-lintfile) · [`--from-trace`](#--from-tracefile) · [`--run-trace`](#--run-tracecmd) · [`--run-timeout`](#--run-timeoutseconds) · [`--note-add`](#--note-addtarget-text) · [`--notes`](#--notes) · [`--pack-task`](#--pack-tasktask) · [`--partition`](#--partitionn) · [`--with-graph`](#--with-graph) · [`--export`](#--exportccjsonfile) · [`--batch`](#--batchfile) -**self-diagnosis** — [`--doctor`](#doctor) · [`--agent`](#agent-codex-claude) · [`--skipped`](#skipped) +**self-diagnosis** — [`--doctor`](#--doctor) · [`--agent`](#--agentcodexclaude) · [`--skipped`](#--skipped) -**security — scan skill files for injection / exfiltration patterns (exit 2 = CRITICAL, 1 = WARN,** — [`--scan-skill`](#scan-skill-file) · [`--scan-skills`](#scan-skills-dir) · [`--force`](#force) +**security — scan skill files for injection / exfiltration patterns (exit 2 = CRITICAL, 1 = WARN,** — [`--scan-skill`](#--scan-skillfile) · [`--scan-skills`](#--scan-skillsdir) · [`--force`](#--force) -**knobs / modes** — [`--rank-by`](#rank-by-pagerank-authority-hub-rrf-churn-churn-decay) · [`--format`](#format-xml-columnar-rows) · [`--format`](#format-candidates) · [`--legend`](#legend-full-compact) · [`--json`](#json) · [`--limit`](#limit-n-offset-m) · [`--exclude`](#exclude-substr) · [`--map-diff`](#map-diff) · [`--cache`](#cache-path) · [`--index-out`](#index-out-base) · [`--no-cache`](#no-cache) · [`--no-ignore`](#no-ignore) · [`--max-file-size`](#max-file-size-n-k-m-g) · [`--refetch`](#refetch) · [`--scip`](#scip-index-scip) · [`--pin-census`](#pin-census-file) · [`--mcp`](#mcp) · [`--listen`](#listen-host-port) · [`--mcp-token`](#mcp-token-t) · [`--allow-remote-edits`](#allow-remote-edits) · [`--eval-stray`](#eval-stray-file) · [`--eval`](#eval) · [`--eval-retrieval`](#eval-retrieval) · [`--eval-mined`](#eval-mined-file) · [`--eval-skills`](#eval-skills-file) · [`-h`](#h-help) · [`-v`](#v-version) +**knobs / modes** — [`--rank-by`](#--rank-bypagerankauthorityhubrrfchurnchurn-decay) · [`--in`](#--indir) · [`--format`](#--formatxmlcolumnarrows) · [`--format`](#--formatcandidates) · [`--legend`](#--legendfullcompact) · [`--json`](#--json) · [`--limit`](#--limitn---offsetm) · [`--exclude`](#--excludesubstr) · [`--map-diff`](#--map-diff) · [`--cache`](#--cachepath) · [`--index-out`](#--index-outbase) · [`--no-cache`](#--no-cache) · [`--no-ignore`](#--no-ignore) · [`--max-file-size`](#--max-file-sizenkmg) · [`--refetch`](#--refetch) · [`--scip`](#--scipindexscip) · [`--pin-census`](#--pin-censusfile) · [`--mcp`](#--mcp) · [`--listen`](#--listenhostport) · [`--mcp-token`](#--mcp-tokent) · [`--allow-remote-edits`](#--allow-remote-edits) · [`--eval-stray`](#--eval-strayfile) · [`--eval`](#--eval) · [`--eval-retrieval`](#--eval-retrieval) · [`--eval-mined`](#--eval-minedfile) · [`--eval-skills`](#--eval-skillsfile) · [`-h`](#-h---help) · [`-v`](#-v---version) --- @@ -4255,13 +4255,50 @@ $ ./build/ripwire . --rank-by=churn --top-k=5 ... [16 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--since` +**Shaped by:** `--since`, `--in` **Caveats (stated by the binary):** - choose the ranking signal: structure, authority, hub, fusion or churn ranking signal (churn = git change-frequency prior, and stamps its own map with rank_by/window/at so it cannot pass for the structural one; - Its age clock is HEAD's OWN commit timestamp, never the wall clock, so the default (whole-history) run is byte-stable for a fixed tree; +### `--in=DIR` + +**Answers:** (with --rank-by=churn-decay) scope the recent-changes block to one directory DIR is root-relative and must be an existing directory under the root (db, src/util; + +a trailing slash is ignored; absolute paths and '..' are refused). The global block stays exactly as without the flag — a change outside DIR is still answered — and a SECOND block follows it with DIR's files only, p= spelled root-relative exactly as the global block spells them, same order (newest commit first). It pages: 40 rows by default, then capped="1" has_more="1" next_offset=N offset=M limit=L and a pasteable next= (--offset=N continues, --limit=N sets the page size). The block's of= IS its total, so the paging half carries no total=. next= replays THIS run's own corpus and window flags (the crawl shapers and the history window), so the page it names is a page of the same answer; a presentation flag is not replayed, since it cannot move of=. next= is absent when that invocation would exceed 120 bytes, and has_more= still says the page exists. The scoped block rides exactly when the global one does: absent means no history was mined, n="0" means none of DIR's files was touched. merge_bombs_skipped= stays on the GLOBAL block only — it counts the window's skipped commits, not DIR's. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all (so the header carries no pr_iters=); would_show= is that same run's own shown= — symbol DEFINITIONS, counted individually exactly as shown= counts them, so the rows that run prints follow from rows+sum(overloads-1)=shown — and next= fetches them. DIR is validated against the CRAWL, not only the filesystem: a case-folded name, a symlink alias and a subtree --exclude dropped are refused rather than answered with an empty block. REFUSED, exactly: beside any flag that answers instead of the scoped map (a report verb, a map-diff run, a body rider, a doctor/batch/server run), under multi-root, beside a --top-k of any value (the map it sizes is the stub), and beside --json. COMPOSES with --limit/--offset, which page the scoped block, and with the byte budgets, which shape the document that is emitted. + +**Try it** + +_Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ + +``` +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 + + + + + + + + + + + + + + +... [8 more line(s); run it to see the whole thing] +``` + +**Shaped by:** `--external-surface`, `--doctor`, `--legend` + +**Caveats (stated by the binary):** + +- absolute paths and '..' are refused). +- It pages: 40 rows by default, then capped="1" has_more="1" next_offset=N offset=M limit=L and a pasteable next= (--offset=N continues, --limit=N sets the page size). +- a presentation flag is not replayed, since it cannot move of=. + ### `--format=xml|columnar|rows` **Answers:** choose the output shape for the flat list verbs output shape for the FLAT list verbs (--callers/--callees/--uses/--impact): xml (default, byte-identical) or columnar (a table + parallel arrays: fields= path,name,line,kind on --callers/--callees/--impact, path,line,role,in_id on --uses — the emitted block's own legend states the zip/n=/,-escape contract; @@ -4342,7 +4379,7 @@ $ ./build/ripwire . --hotspots --json (empty) ``` -**Shaped by:** `--max-tokens`, `--token-budget`, `--for` +**Shaped by:** `--max-tokens`, `--token-budget`, `--for`, `--in` **Caveats (stated by the binary):** @@ -4412,7 +4449,7 @@ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 ... [10 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--skipped`, `--index-out` +**Shaped by:** `--skipped`, `--in`, `--index-out` ### `--map-diff` diff --git a/docs/EVALS.md b/docs/EVALS.md index 360d7698..b2eb8f4d 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 615 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 616 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5834,7 +5834,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **615 gate scripts**, all of which exist on disk. +naming **616 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6846,7 +6846,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 615. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 616. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LIMITS.md b/docs/LIMITS.md index b9b26bf7..15cff728 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -16,10 +16,10 @@ it once, marked `×N`. | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 210 | 84 | 116 | **94** | +| 211 | 84 | 117 | **94** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 210 + 7 is the 217 constants this generator parses out of `src/`. +are not counted as caps, and 211 + 7 is the 218 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -37,8 +37,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **113 of 210 caps are classified -(38 INDEXING, 40 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **114 of 211 caps are classified +(39 INDEXING, 40 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -88,7 +88,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 84 files that declare a cap — the 210 caps counted above, and no parameter. +One table for each of the 84 files that declare a cap — the 211 caps counted above, and no parameter. ### `src/abicheck.h` @@ -325,6 +325,7 @@ Discloses: `coboost_commits_capped`, `coboost_partners_capped` | constant | value | class | note | | --- | --- | --- | --- | +| `kChurnMergeBombMaxFiles` | `100` | INDEXING | the churn rankers' merge-bomb rule; skipped commits are disclosed as | | `kCoBoostMaxFilesPerCommit` | `30` | INDEXING | same bulk-commit cap as the other co-change miners here | | `kCoBoostMaxPartnerFiles` | `8` | INDEXING | strongest partners only, by (deg desc, path asc) | | `kCoBoostMaxSymbolsPerFile` | `3` | INDEXING | per partner file: its top-3 symbols by (lens score desc, id asc) | diff --git a/docs/TUNING.md b/docs/TUNING.md index a5c0c0cc..4bcaf7e8 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,18 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 128 | 127 | 112 | 12 | **37** | 75 | - -The first two columns are not the same number, and the gap is not a rounding: `src/` holds -**128 cap declarations** under **127 distinct names** (`kRowCap` declared in more than one file). The -sweep patches by NAME, so `112 + 12` accounts for the 127 NAMES — not the 128 declarations. Quoting -"113 of 128" would be wrong in both halves at once, which is exactly the shape of error a -generated table exists to prevent. +| 129 | 128 | 112 | 12 | **37** | 75 | + +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **129 cap +declarations** under **128 distinct names** (`kRowCap` declared in more than one file). The sweep +patches by NAME, so its own population is NAMES and not declarations — and the two columns beside +those are frozen at the commit named under Provenance while this census is re-read from `src/` on +every run, which makes them a third population again. Enumerated over the 128 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **5 declared since the sweep was prepared, +which no measurement has touched** (`kChurnMergeBombMaxFiles`, `kFieldIdCapacity`, +`kForPageRowsDefault`, `kForPageUnionSymbolCap`, `kMaxBlockBytes`). 111 + 12 + 5 = 128, and `emit` +refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than +111 because one name the sweep classified is no longer declared in `src/` at all: +`kSituTestRowsShown`. Quoting "113 of 129" would be wrong in both halves at once, which is the shape +of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 128-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 129-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/docs/captures/COMMANDS_showcase_2026-09-12.md b/docs/captures/COMMANDS_showcase_2026-09-12.md index 9898fbb6..62db2376 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-12.md +++ b/docs/captures/COMMANDS_showcase_2026-09-12.md @@ -3375,6 +3375,43 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s ````` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~7 --exclude=test --exclude=docs --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + ## `./build/ripwire . --rank-by=bogus --top-k=5` *An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* diff --git a/docs/captures/COMMANDS_showcase_2026-09-13.md b/docs/captures/COMMANDS_showcase_2026-09-13.md index 2a9b161c..f9fee964 100644 --- a/docs/captures/COMMANDS_showcase_2026-09-13.md +++ b/docs/captures/COMMANDS_showcase_2026-09-13.md @@ -3416,6 +3416,35 @@ ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s ````` +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + ## `./build/ripwire . --rank-by=bogus --top-k=5` *An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* diff --git a/docs/docs_commands_build.py b/docs/docs_commands_build.py index 0f982b23..d81c19d6 100644 --- a/docs/docs_commands_build.py +++ b/docs/docs_commands_build.py @@ -562,8 +562,39 @@ def shaped_by( entry, sections ): # ── rendering ───────────────────────────────────────────────────────────────────────────────────── -def anchor_of( spec ): - return re.sub( r'[^a-z0-9]+', '-', spec.lower() ).strip( '-' ) +# THE ANCHOR THE HEADING ACTUALLY MINTS, which is not the one this function used to compute. Every +# section is rendered as ``### `SPEC` ``, and the renderer (GitHub's slugger, and markdownlint's MD051 +# with it) lower-cases the heading text, DELETES every character that is not a word character, a hyphen +# or a space, then turns the spaces into hyphens. Nothing is substituted and nothing is trimmed, so +# `--in=DIR` becomes `--indir`: the two leading dashes survive and the `=` is gone. +# +# The old spelling replaced every RUN of non-alphanumerics with a hyphen and stripped the ends +# (`--in=DIR` -> `in-dir`) — a plausible slug, and not this document's. CodeRabbit named it on the --in +# row; the real count is the whole table of contents (all 169 links resolved to nothing), which is also +# why the report arrived as 28 MD051 warnings on a single line. Gate: docscommandscheck arm (J), which +# states the renderer's rule INDEPENDENTLY rather than importing this function — a gate that asks the +# generator what the anchor should be agrees with whatever the generator says. +def anchor_of( heading_text ): + return re.sub( r'[^\w\- ]', '', heading_text.strip().lower() ).replace( ' ', '-' ) + + +def assign_anchors( sections ): + """Give every entry the anchor its own heading will mint, in DOCUMENT order. + + Per ENTRY and not per spec, because the renderer's rule for a repeated heading is to append `-1` + to the second one, and only a walk in emission order can know which one IS the repeat. No spec + repeats today (measured: 176 entries, 180 headings, no duplicate slug), so the suffix branch is + dormant — what it buys is that a repeat lands as TWO anchors the renderer agrees with, instead of + two contents links pointing at the first section. Recomputing from the spec in the contents loop + cannot do that, and the wrong link it would emit resolves, so no anchor gate would see it. + """ + seen = {} + for _title, entries in sections: + for entry in entries: + base = anchor_of( '`%s`' % entry[ 'spec' ] ) # backticks fall to the same rule that drops `=` + n = seen.get( base, 0 ) + seen[ base ] = n + 1 + entry[ 'anchor' ] = base if n == 0 else '%s-%d' % ( base, n ) # The --recall FLAG's own --help text says nothing about pointing it at a directory that is not a @@ -697,12 +728,13 @@ def render( name, preamble, sections, captures, capturePath ): w( '' ) # ── table of contents ── + assign_anchors( sections ) # in emission order: the anchors below are read, never recomputed w( '## Contents' ) w( '' ) for title, entries in sections: if not entries: continue - w( '**%s** — %s' % ( title, ' · '.join( '[`%s`](#%s)' % ( e[ 'flags' ][ 0 ], anchor_of( e[ 'spec' ] ) ) + w( '**%s** — %s' % ( title, ' · '.join( '[`%s`](#%s)' % ( e[ 'flags' ][ 0 ], e[ 'anchor' ] ) for e in entries ) ) ) w( '' ) diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index 8f70b62d..5f971203 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -24,6 +24,7 @@ # these OUTPUT would ask for a disclosure that could never fire, which is a different kind of dishonesty # from the silent cut this register was built to find. kChaConeCap INDEXING +kChurnMergeBombMaxFiles INDEXING kCoBoostMaxFilesPerCommit INDEXING kCoBoostMaxPartnerFiles INDEXING kCoBoostMaxSymbolsPerFile INDEXING diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index f45c6064..a0c7c6ea 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -191,7 +191,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["byte-identical", "two runs, same bytes — a gate on every push, not a tendency; warm equals cold"], ["zero runtime deps", "CMake + a C++23 compiler; builds with the network off — vendored everything"], ["the languages", "Rust · C++ · ObjC/C++ · C · Metal · CUDA · Python · Go · Swift · TypeScript · JavaScript · Java · Kotlin · Ruby · PHP · Lua · Elixir · Dart · Bash · C# · JSON · TOML · YAML · Markdown — 24 vendored grammars; markdown headings are real symbols"], - ["agent-native", "an MCP server and 179 long flags behind one `--help` that is always the authority"], + ["agent-native", "an MCP server and 180 long flags behind one `--help` that is always the authority"], ]; // Row height carries the LONGEST body (the language line, which wraps to three at this width), // not the shortest — a fixed 0.68 clipped its last line off the bottom of the card. @@ -226,7 +226,7 @@ function storyCards(s, { kick, head, stories, footText }){ s.addText(flags, { x: MX+6.3, y: y+0.04, w: 5.65, h: 0.58, fontFace: MONO, fontSize: 9, color: CYAN, valign: "middle", margin: 0 }); y += 0.74; } - foot(s, "--help is generated from the binary's own flag table — 179 long flags; docs/COMMANDS.md carries an entry for every one of them, 161 with a recorded invocation and its output"); + foot(s, "--help is generated from the binary's own flag table — 180 long flags; docs/COMMANDS.md carries an entry for every one of them, 161 with a recorded invocation and its output"); } /* ── S5 · the moments ───────────────────────────────────────────────────── */ @@ -1068,7 +1068,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["615 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["616 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1092,7 +1092,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "615", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "616", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1339,10 +1339,10 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// do not take any of it on trust", AMBER); title(s, "Every claim, and the command that re-derives it"); const claims = [ - ["179 long flags · 33 slides", "bash test/deckclaimcheck.sh"], + ["180 long flags · 33 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["615 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["616 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 70 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/cli.h b/src/cli.h index f96ef9ed..2d9c6958 100644 --- a/src/cli.h +++ b/src/cli.h @@ -517,6 +517,9 @@ struct Config // per drop (oversize/excluded/unsupported-ext) plus rows for // indexed-but-suspect files (degraded-parse/minified-suspect) and // per unindexed extension. Read-only; exit 0 always. + std::string_view inDir; // --in=DIR (C1-b, 2026-09-12): with --rank-by=churn-decay, a root-relative directory — + // a second block follows the unchanged global one and the symbol + // map collapses to a counted stub; refused with any other verb (validateModifierGuards) std::string_view since; // --since=REV|DATE: scope churn/co-change mining to commits after this point // (--hotspots/--cochange/--rank-by=churn). REV (e.g. HEAD~20, a tag) is // deterministic; a git approxidate ("2 weeks ago") is wall-clock-relative @@ -2352,6 +2355,33 @@ inline constexpr char kHelpTail[] = " counted equally, so recent edits outweigh old ones. Its age clock is HEAD's OWN commit\n" " timestamp, never the wall clock, so the default (whole-history) run is byte-stable for a\n" " fixed tree; the half-life is disclosed in window=. default pagerank)\n" + " --in=DIR (with --rank-by=churn-decay) scope the recent-changes block to one directory\n" + " DIR is root-relative and must be an existing directory under the root (db, src/util;\n" + " a trailing slash is ignored; absolute paths and '..' are refused). The global \n" + " block stays exactly as without the flag — a change outside DIR is still answered — and a\n" + " SECOND block follows it with DIR's files only, p= spelled\n" + " root-relative exactly as the global block spells them, same order (newest commit first).\n" + " It pages: 40 rows by default, then capped=\"1\" has_more=\"1\" next_offset=N offset=M\n" + " limit=L and a pasteable next= (--offset=N continues, --limit=N sets the page size). The\n" + " block's of= IS its total, so the paging half carries no total=. next= replays THIS run's\n" + " own corpus and window flags (the crawl shapers and the history window), so the page it\n" + " names is a page of the same answer; a presentation flag is not replayed, since it cannot\n" + " move of=. next= is absent when that invocation would exceed 120 bytes, and has_more= still\n" + " says the page exists. The scoped block rides exactly\n" + " when the global one does: absent means no history was mined, n=\"0\" means none of DIR's\n" + " files was touched. merge_bombs_skipped= stays on the GLOBAL block only — it counts the\n" + " window's skipped commits, not DIR's. The symbol map collapses to a disclosed stub\n" + " — the map was not asked for and was not ranked\n" + " at all (so the header carries no pr_iters=); would_show= is that same run's own shown= —\n" + " symbol DEFINITIONS, counted individually exactly as shown= counts them, so the rows that\n" + " run prints follow from rows+sum(overloads-1)=shown — and next= fetches them.\n" + " DIR is validated against the CRAWL,\n" + " not only the filesystem: a case-folded name, a symlink alias and a subtree --exclude\n" + " dropped are refused rather than answered with an empty block. REFUSED, exactly: beside any\n" + " flag that answers instead of the scoped map (a report verb, a map-diff run, a body rider,\n" + " a doctor/batch/server run), under multi-root, beside a --top-k of any value (the map it\n" + " sizes is the stub), and beside --json. COMPOSES with --limit/--offset, which page the\n" + " scoped block, and with the byte budgets, which shape the document that is emitted.\n" " --format=xml|columnar|rows choose the output shape for the flat list verbs\n" " output shape for the FLAT list verbs (--callers/--callees/--uses/--impact):\n" " xml (default, byte-identical) or columnar (a table + parallel arrays: fields=\n" @@ -2972,6 +3002,7 @@ inline constexpr ViewFlag kViewFlags[] = { "--cache=", &Config::cacheFile , EmptyValue::Refuse, "a cache file path", "--cache=.ripwirecache" }, { "--index-out=", &Config::indexOut , EmptyValue::Refuse, "a base path for the index artifacts", "--index-out=.ripwire/index" }, { "--since=", &Config::since , EmptyValue::Refuse, "a git revision or date", "--since=HEAD~20" }, + { "--in=", &Config::inDir , EmptyValue::Refuse, "a root-relative directory under the root", "--in=src" }, { "--scip=", &Config::scipIndex , EmptyValue::Refuse, "a SCIP index file path", "--scip=index.scip" }, { "--pin-census=", &Config::pinCensus , EmptyValue::Refuse, "a census output file path", "--pin-census=census.tsv" }, @@ -3252,7 +3283,7 @@ inline constexpr IntFlag kIntFlags[] = // warn once per RUN, not per flag — state a BoolFlag row has nowhere to keep) // • a bare no-op / bare pair --route, --quality-ack (the =REASON form is a kViewFlags row) inline constexpr std::size_t kHandWrittenFlagArms = 22; // +1: --color-by= (enum-value arm); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (repeatable-value arms, same shape as --exclude=); +1 R-H: --grep-in= (closed-value arm, same shape as --grep-scope=) -inline constexpr std::size_t kTotalFlagArms = 209; // +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it +inline constexpr std::size_t kTotalFlagArms = 210; // +1 lane/recent-scope (2026-09-12, C1-b): --in= (kViewFlags row) — the directory-scoped block of --rank-by=churn-decay; +2 P4 (capture-audit 2026-09-04, lane L7): --zoom-levels= (kIntFlags row, the printed-levels ceiling) and --include-builtins (kBoolFlags row, the external-surface builtin opt-in); +1 P9 (capture-audit 2026-09-04, lane L8): --no-post-check (kBoolFlags row, the edit receipt's folded verification opt-out); +1 lane/ca-L2 (2026-09-04, H11): --allow-dirty (kBoolFlags row) — the explicit consent --quality-baseline needs before it pins a floor on a tree that differs from HEAD; +1 lane/n6-c (2026-09-03): --no-ignore (kBoolFlags row, the .gitignore-by-default escape hatch); +1 lane/af-scope (2026-08-29): --scope= (kViewFlags row, the quality-delta ownership partition); +1 --quality-delta= (kViewFlags, R-I ref-pair form); +1 --help-task= (kViewFlags); +2 VT-1: --run-trace= (kViewFlags) and --run-timeout= (kIntFlags); +1: --handoff (kBoolFlags row); +1 --readability (kBoolFlags row); +2 §CLIO: --cochange-groups (kBoolFlags), --cochange-recur= (kIntFlags); +1 --context-ratio (kBoolFlags row); +1 --nonlocal-state (kBoolFlags row); +2 --field-affinity (kBoolFlags) and --field-affinity= (kViewFlags); +1 --comment-coherence (kBoolFlags row); +2 --dmm (kBoolFlags) and --dmm= (kViewFlags); +2 --quality-panel (kBoolFlags) and --quality-panel= (kViewFlags); +1 --naming-consistency (kBoolFlags row); +1 --naming-locals (kBoolFlags row, local-variable-indexing plan Phase 2); +1 --skipped (kBoolFlags row, §P0.5d itemization); +1 --with-profile= (kViewFlags row, the --lint × #PROF_TSV heat join); +1 --color-by= (hand-written enum-value arm); +1 --sarif (kBoolFlags row, W1-SARIF: SARIF 2.1.0 export for --lint); +1 --signatures-only (kBoolFlags row, T3 terminal-by-default --for opt-out); +3 L7: --lint-catalog (kBoolFlags), --lint-select= and --lint-ignore= (kViewFlags); +3 G3 (2026-08-15 harvest): --and=/--not=/--grep-scope= (hand-written arms); +1 R-H: --grep-in= (hand-written arm); +1 R2: --pattern= (kViewFlags row, the code-shaped structural search); +1 lane/safe-delete (2026-08-21): --safe-delete= (kViewFlags row, the composed "can I delete this?" read); +1 lane/compact-conceptual (2026-08-22): --auto-bodies (kBoolFlags row, the compact-conceptual-serving opt-out); +5 CLI edit bridge (2026-08-27): --replace-symbol-body=/--insert-before-symbol=/--insert-after-symbol=/--edit-payload=/--edit-target-file= (kViewFlags rows); +1 --handles (kBoolFlags row, grep edit handles); +1 --legend= (kViewFlags row, compact schema dialect); +3 edit-plan: --edit-plan= (kViewFlags) and --dry-run/--apply (kBoolFlags rows); +1 --agent= (kViewFlags row, the --doctor Codex surface); +1 lane/paper-slice (2026-08-28): --slice= (kViewFlags row, the ARISE-motivated def-use slice); +1 lane/af-planlint (2026-08-29): --plan-lint= (kViewFlags row, the PLAN-format structure gate, P3.2); +2 lane/or-arise (2026-08-30): --slice-flow= (kViewFlags row) and --slice-depth= (kIntFlags row) — the ARISE rung-2 cross-statement data-flow slice; +1 lane/at-seed (2026-08-30): --at= (kViewFlags row) — the FILE:LINE enclosing-chain report, with the @FILE:LINE selector spelling resolved in graph.h (no flag arm of its own); +1 CARD-1 phase 2 (2026-08-31): --pin-census= (kViewFlags row) — the eval-only S6-C silent-pin census, written beside the map and never into it static_assert( std::size( kBoolFlags ) + std::size( kViewFlags ) + std::size( kIntFlags ) + kHandWrittenFlagArms == kTotalFlagArms, "a --flag arm was added or removed without updating the ledger above — count the arms in parseArgs and fix the counter" ); @@ -3471,6 +3502,10 @@ inline bool honorsPaging( const Config& c ) noexcept || !c.editCheckSym.empty() // 2026-09-10: --edit-check windows its UNFLAGGED caller rows (editcheck.h) || c.darkFlags // 2026-09-10 (C1 F-07): --flags' per-gate sites, and --flip's six listings || c.situ || !c.situFiles.empty() // 2026-09-10 (C1 F-10): --situ sections [1] and [3] (section [2] is the answer) + // --in=DIR is deliberately NOT a member, and was: it is a MODIFIER of the default map, not a verb, and + // membership made validateShapingFlagsHonored refuse every --top-k/--max-tokens/--token-budget beside it + // with a message that hands the caller a list of VERBS and claims the default map honours the budgets it + // had just refused. The scoped ELEMENT pages (validatePagingHonored's carve-out below); the run does not. // L-W (2026-09-12, forpage.h): --for joins ONLY when a window is asked for — --limit/--offset select its // FILE-GRAIN widening page. Membership is conditional on purpose: the bare --for bundle keeps honoring // --token-budget/--max-tokens/--format=candidates --top-k, which validateShapingFlagsHonored refuses on @@ -3535,6 +3570,14 @@ inline void validatePagingHonored( Config& c ) noexcept { return; } + // C1-b: --in=DIR windows an ELEMENT, not a verb — the map's block is what --limit/--offset + // cut, and the run around it is still the default map. That is why --in is not a kPagingHonoringVerbs row (a + // row would make the shaping guard refuse the budgets the map does honour): the honoring decision lives here, + // where it is about the flags typed, and the refusal above skips it. + if( !c.inDir.empty() ) + { + return; + } // §B11.6: name the MODE first when one is what disabled paging — the honoring list below is otherwise // read as self-contradicting, and the remedy ("drop the mode flag") is not derivable from it. @@ -4037,6 +4080,13 @@ inline constexpr SinceHost kSinceHosts[] = { { "--slice", true }, }; +// --in=DIR's HOSTS, declared the way kSinceHosts declares --since's and for the same reason: --in scopes a +// recent-changes BLOCK, and only a verb that HAS such a block can be scoped. Written as a table rather than as +// `c.rankBy != RankBy::ChurnDecay` inline so that the planned `--for --in=DIR` is ONE ROW here and inherits +// both decisions that read it — this refusal, and main.cpp's derived "did something answer instead" sweep — +// instead of a second copy of the host list that a later consumer can contradict by omission. +inline constexpr std::string_view kInHosts[] = { "--rank-by=churn-decay" }; + // Is this host the one the run selected? One switch, beside the table it switches on, so the two cannot // drift; a flag this function does not know is never active. inline bool sinceHostActive( const Config& c, std::string_view flag ) noexcept @@ -4049,6 +4099,20 @@ inline bool sinceHostActive( const Config& c, std::string_view flag ) noexcept return false; } +// Is one of --in's hosts the verb this run selected? It REUSES the switch above rather than writing a second +// one: every kInHosts row is a history-windowed verb, which is the same property kSinceHosts rows have, and +// two switches over the same question is the clone --quality-delta catches (it caught exactly that here). +// A kInHosts row must therefore also be a kSinceHosts row — true of --rank-by=churn-decay, and true of the +// planned --for --in=DIR only once --for takes --since, which is the honest order for both flags anyway. +inline bool inHostActive( const Config& c ) noexcept +{ + // The static_assert, not a loop, is what keeps this honest at one row: a loop over a one-row table is a + // clone of every other any_of-over-a-table in the tree (--quality-delta paired it with two), and a second + // row here must also grow the refusal's prose, which the assert forces someone to notice. + static_assert( std::size( kInHosts ) == 1, "--in=DIR reads its single host here and names it in prose below — extend both together" ); + return sinceHostActive( c, kInHosts[0] ); +} + // The two questions asked of this table are the SAME walk over it with one predicate switched, so they are // one function: "is any host selected" and "is the selected host a baseline host". Written as two loops // first, and --quality-delta named the second a 37-token clone of the first the same afternoon. @@ -4227,6 +4291,47 @@ inline void validateModifierGuards( Config& c ) noexcept c.ok = false; } + // --in=DIR (C1-b, 2026-09-12) scopes a recent-changes BLOCK to one directory, and only a HOST that has such a + // block can be scoped — kInHosts above is that list, so this reads the table instead of spelling one host by + // hand. Same rule as --since: a run where the flag would do nothing refuses instead, naming the remedy. + // + // THE REFUSAL SET, in full, because --help and the CHANGELOG state exactly these and nothing more: + // not a host (--rank-by is anything but churn-decay, or absent) + // multi-root the block --in scopes is single-root only, so there is none to scope + // --top-k=N ANY N, not only 0. Under --in the symbol map is a STUB, and --top-k sizes exactly the + // rows the stub does not print — so the flag shapes nothing, whatever its value. It used + // to be refused only at 0, and at 0 it was refused THREE times over (this arm, plus two + // from validateShapingFlagsHonored, which --in has now left — see honorsPaging). + // --json named by the --json allow-list walk (jsonUnsupportedVerb), not here. + // NOT refused, and documented as composing: --limit/--offset (they window the scoped block — see + // validatePagingHonored) and --max-tokens/--token-budget (they shape the document that IS emitted). + // + // A chain of `else if`, not three independent guards: one bad combination gets one message. The directory + // itself is checked twice in main.cpp — against the filesystem (inDirIsUnderRoot, the cheap first reject) and + // then against the CRAWL's own spellings, which is the only check that can tell DIR from a typo. + if( !c.inDir.empty() ) + { + if( !inHostActive( c ) ) + { + // The sentence names the single host in kInHosts (inHostActive's static_assert pins the count). + rw::emitTo( stderr, "ripwire: --in=DIR scopes the recent-changes block of {} to one directory — pass it " + "(e.g. ripwire --rank-by=churn-decay --in=src)\n", kInHosts[0] ); + c.ok = false; + } + else if( c.roots.size() >= 2 ) + { + rw::emitRaw( stderr, "ripwire: --in=DIR scopes a single-root map; the block it scopes is absent under multi-root — pass one root (e.g. ripwire --rank-by=churn-decay --in=src)\n" ); + c.ok = false; + } + else if( c.topKExplicit ) + { + rw::emitRaw( stderr, "ripwire: --in=DIR stubs the symbol map, and --top-k=N sizes exactly the rows the stub does not print — drop one of them " + "(e.g. ripwire --rank-by=churn-decay --in=src, or drop --in to size the map). --limit/--offset page the scoped block; " + "--max-tokens/--token-budget shape the document that is emitted\n" ); + c.ok = false; + } + } + // --cochange-recur=K / --cochange-groups are read ONLY inside the --cochange branch of // runMaintenanceViews (main.cpp). Alone they silently no-op; refuse loudly, exactly like --since above. if( ( c.cochangeRecur > 0 || c.cochangeGroups ) && !c.cochange ) @@ -4628,7 +4733,9 @@ inline void validateConfig( Config& c ) noexcept rw::emitRaw( stderr, "ripwire: --recall --top-k=0 means \"emit zero documents\" — raise it (--top-k=N) or drop it for the default of 8\n" ); c.ok = false; } - else if( c.topK == 0 && c.expand.empty() && c.outline.empty() && !c.packSignatures && c.packTopN <= 0 ) + // C1-b: --in=DIR answers the whole --top-k question in ONE message of its own (validateModifierGuards), so + // this generic arm stays quiet beside it — two refusals for one mistake is the shape the review named. + else if( c.topK == 0 && c.inDir.empty() && c.expand.empty() && c.outline.empty() && !c.packSignatures && c.packTopN <= 0 ) { rw::emitRaw( stderr, "ripwire: --top-k=0 means \"no ranked map, payload only\" — pass a payload verb (--expand=SYM / --outline=SYM / --pack-signatures / --pack-top-n=N), or use --top-k=1 for the smallest map\n" ); c.ok = false; diff --git a/src/compactlegend.h b/src/compactlegend.h index e0e9da6a..2f790009 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -230,6 +230,10 @@ inline constexpr std::string_view kCompactProsePrefixes[] = ""; + "commit, at HEAD's clock) w= (its decayed weight), age_d asc then w desc then path; absent under multi-root. " + "merge_bombs_skipped= counts the commits in the mined window that touched more than 100 INDEXED files (files this " + "crawl holds, never the commit's raw file count) and were SKIPPED, uncounted (bulk sweeps, wide merges): a file only " + "such a commit touched is absent from these rows and from the prior, so a 0 means no commit was skipped, never that " + "none could be. It counts the WINDOW's commits, so it rides this block only -->"; +static_assert( kChurnMergeBombMaxFiles == 100, "kChurnDecayRankLegend spells the merge-bomb threshold as 100 — move both together" ); + +// C1-b (2026-09-12): the in=DIR clause, spliced only when the scoped block is present (zero bytes elsewhere). Two halves around +// kNextLegendClause, the ONE definition of next= every legend that meets it splices. No "--" inside a comment (G4). +inline constexpr const char* kRecentScopeLegendOpen = + ""; // Which churn legend belongs to which churn ranker — the table-driven form the sibling rankBy lookup uses, // so a third churn variant adds a row and not a branch. @@ -2184,22 +2288,32 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto return rootArg.empty() ? std::string_view( ing.files[ fileId ] ) : rw::sarif::rootRelativeUri( ing.files[ fileId ], rootPrefix ); }; + // C1-b: `keep` is the row count the map WOULD print, and it depends on topK and S alone — never on the + // ranking. So under in=DIR's stub it is still the honest would_show=, and every step that exists only to + // ORDER and BUCKET rows nobody prints is skipped: the radix sort over S symbols, the files-sized bucket + // allocation, and the fill loop. (The loop below already walked an empty order; this is the work that + // ran to feed it.) + const std::size_t keep = std::min( topK > 0 ? std::size_t( topK ) : S, S ); + const bool stubbed = ann.stubSymbols; + // rank order: (rank desc, id asc) — the id tie-break makes the top-K deterministic. - std::vector order( S ); - for( NodeId i = 0; i < S; ++i ) + std::vector order; + if( !stubbed ) { - order[i] = i; + order.resize( S ); + for( NodeId i = 0; i < S; ++i ) + { + order[i] = i; + } + sortutil::radixSortByScoreDescId( order, rank ); } - sortutil::radixSortByScoreDescId( order, rank ); - - const std::size_t keep = std::min( topK > 0 ? std::size_t( topK ) : S, S ); // bucket the kept symbols by file, files ordered by their best (first-seen) rank. - std::vector> buckets( ing.files.size() ); + std::vector> buckets( stubbed ? 0 : ing.files.size() ); std::vector fileOrder; - std::vector seen( ing.files.size(), 0 ); + std::vector seen( stubbed ? 0 : ing.files.size(), 0 ); - for( std::size_t k = 0; k < keep; ++k ) + for( std::size_t k = 0; !stubbed && k < keep; ++k ) { const NodeId id = order[k]; const Symbol& s = ing.symbols[id]; @@ -2217,7 +2331,10 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // T3 note: the auto-order decision keys off the MAP's own estimate, NOT the payload — the fill-order // heuristic reasons about the map that gets reordered; the appended // blocks are // emitted after and cannot be reordered, so they must not shift the map's primacy/recency decision. - const TokenEstimate mapEst = estimateTokens( ing, order, keep, outOff, outTargets ); + // C1-b: under the stub the map prints NO symbol rows, so the estimate is taken over none of them. It used + // to be taken over `keep` rows the document does not contain, which put an est_tokens= in the header for a + // payload that was not there — and paid a per-symbol pass to compute it. + const TokenEstimate mapEst = estimateTokens( ing, order, stubbed ? 0 : keep, outOff, outTargets ); const std::size_t mapEstTokens = mapEst.tokens; // T3: fill-aware auto important-last. A PURE function of estTokens (itself a pure function of the @@ -2298,6 +2415,12 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto if( churnWindow != nullptr ) { legend += churnRankLegendFor( ann.churnRankLabel ); // §A9.6 / P0-4, churn-only (see the constants) + if( ann.scopedRecent != nullptr ) + { + legend += kRecentScopeLegendOpen; // C1-b: in=DIR — the scoped block, its page, and the map stub (present-only) + legend += kNextLegendClause; + legend += kRecentScopeLegendClose; + } } // §B2.1: the same treatment for authority/hub/rrf. Mutually exclusive with the churn arm by construction // (main.cpp fills exactly one of the two fields), and null on the default pagerank map ⇒ zero bytes there. @@ -2438,7 +2561,7 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto stats += std::to_string( ing.files.size() ); stats += " symbols="; stats += std::to_string( S ); stats += " edges="; stats += std::to_string( outTargets.size() ); - stats += " shown="; stats += std::to_string( keep ); + stats += " shown="; stats += std::to_string( ann.stubSymbols ? std::size_t( 0 ) : keep ); // C1-b: the stub prints no row stats += " est_tokens="; stats += std::to_string( estTokens ); stats += " ambiguous="; stats += std::to_string( ambTotal ); stats += " unresolved="; stats += std::to_string( unresolvedTotal ); @@ -2568,7 +2691,46 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto } } writeRecentRows( w, ann, pathRel, esc ); // F3: rank_by=churn-decay's file-level answer, before the symbol map - for( std::uint32_t f : fileOrder ) + // C1-b: under in=DIR the symbol map is a DISCLOSED stub (docs/METHODOLOGY.md §9.3) — the caller asked for DIR's + // recent files, not the map. + // + // NOT total=/shown=, which is what this first shipped as. Under pageview.h's vocabulary total= is THE TOTAL + // (rule 2) and capped= always rides beside a shown= (rule 3), and the stub had neither property: it printed + // total="200" — the --top-k PAGE SIZE — on a document whose own header says symbols="18457", with no capped=, + // so the one number it carried was the one number it was not allowed to mean. The stub is not a page of the map + // and it must not borrow the page vocabulary to say so. It says what it IS instead: + // stubbed="1" the symbol map was not rendered at all (the loop walks an empty order below) + // would_show="N" `keep` — the un-stubbed header's own shown=, and shown= counts symbol DEFINITIONS + // individually, not printed rows: the print loop runs collapseOverloadRows() per file + // bucket, so a const/non-const pair that both make the top-K cut prints ONE row carrying + // overloads=2. The row count therefore FOLLOWS from the identity the map legend already + // publishes for shown= — rows+sum(overloads-1)=shown — rather than being reported here. + // Measured on this repo 2026-09-14: shown=200, 193 rows, 7 at overloads=2, 193+7=200. + // IT IS A DEFINITION COUNT AND NOT A CEILING, by owner decision 2026-09-14, and the + // reason is vocabulary: in this tool a floor/ceiling marker means "we could not see + // everything" (counts_floor=, _capped, the truncation disclosures). would_show is EXACT; + // what differs from a reader's guess is the UNIT. Spending an uncertainty marker on a + // unit difference would make "ceiling" mean "exact, but not in the unit you assumed" and + // weaken every honest use of the word elsewhere in the output. Naming the quantity is + // also the stronger claim: a ceiling cannot be inverted, while a definition count plus + // the published identity yields the rows. Reporting post-collapse rows instead — the + // review's original ask — is not available at this site for any price worth paying: + // which definitions survive the cut is a fact about the RANKING, and not ranking is the + // whole point of the stub (no pr_iters= rides its header). Not the corpus total either + // (that is symbols=), so the three cannot be read as each other. + // test/recentscopecheck.sh arms 14a/14b pin the arithmetic. + // Rule 3's own sentence sanctions the shape: "If a verb emits no shown=, it emits no capped= either." + if( ann.stubSymbols ) + { + std::string stub = " kNoFiles; + for( std::uint32_t f : ann.stubSymbols ? kNoFiles : fileOrder ) { w.write( " block comes FIRST (H2H-Graft F3). +# 7 merge_bombs_skipped= on : a commit touching more than 100 files is skipped by the miner, and +# the block SAYS how many it skipped (always, "0" included) — a >100-file fixture commit reads "1"; a window +# whose ONLY commit was skipped still prints the block, with zero rows and the count (7h). # # Determinism note: symbol ORDER is compared, never k= floats (CONTRIBUTING §3 — a sort has no tolerance # band, a float does; this gate uses the sort). @@ -169,8 +173,14 @@ fi # — new.py (2 commits at HEAD's day, weight ≈ 2.0) before old.py (6 commits 400 days back, ≈ 0.28) — with # age_d= on HEAD's clock and the weight the ranker used, and it precedes every group. R6="$( perl -e 'alarm 20; exec @ARGV' "$BIN" "$WORK/recent" --rank-by=churn-decay --no-cache 2>/dev/null )" -printf '%s' "$R6" | grep -q '' \ - && ok "arm 6a: is emitted" || no "arm 6a: no element" +# The CLOSED attribute set, not a prefix. This arm was loosened to a prefix match when merge_bombs_skipped= +# landed, and a prefix cannot see an attribute ADDED after of= — which is exactly how the window's +# merge_bombs_skipped= came to be stamped on the scoped block too, unnoticed by any arm here. arm 7h pins only +# the all-bomb shape (n="0" of="0"), so this is the one arm that pins a rows>0 global block's whole tag. +r6_tag="$( printf '%s' "$R6" | grep -oE ']*>' | head -1 )" +[ "$r6_tag" = '' ] \ + && ok "arm 6a: the global block's tag is exactly $r6_tag (closed set: n=, of=, merge_bombs_skipped=)" \ + || no "arm 6a: global block tag is '$r6_tag', expected " r6_first="$( printf '%s' "$R6" | grep -oE ' is '$r6_first', expected new.py" @@ -189,5 +199,89 @@ R6p="$( perl -e 'alarm 20; exec @ARGV' "$BIN" "$WORK/recent" --rank-by=churn --n printf '%s' "$R6p" | grep -q ' (churn-decay only)" \ || ok "arm 6f: plain churn carries no " +# ── arm 7: merge_bombs_skipped= — the cut the miner makes is DISCLOSED on the block it shapes ──────────── +# The decayed walk skips any commit touching more than 100 files (the merge-bomb rule) and, until this arm, +# counted NOTHING about it: a block could omit the very commit a question was about (a held-out gold +# commit with 71 src files was invisible) and nothing in the output said a commit had been dropped. The +# fixture: three ordinary commits on small.py, then ONE commit adding 101 files under bulk/. The block must +# say merge_bombs_skipped="1", the bulk-only files must be ABSENT from its rows (that is what "skipped" means), +# the plain fixture must say "0" (absence is never ambiguous), and both legends must define the attribute. +# RED against the pre-change binary: no merge_bombs_skipped= anywhere. +BOMB="$WORK/bomb"; mkdir -p "$BOMB/bulk" +git -C "$BOMB" init -q 2>/dev/null +git -C "$BOMB" config user.email rw@example.invalid +git -C "$BOMB" config user.name ripwire-gate +for i in 1 2 3; do + stamp="$(( RECENT_BASE + i * 86400 ))" + printf 'def small_one():\n return %d\n' "$i" > "$BOMB/small.py" + GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$BOMB" add -A >/dev/null 2>&1 + GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$BOMB" commit -q -m "small $i" >/dev/null 2>&1 +done +i=0 +while [ "$i" -le 100 ]; do + printf 'def bulk_%03d():\n return %d\n' "$i" "$i" > "$BOMB/bulk/b$( printf '%03d' "$i" ).py" + i=$(( i + 1 )) +done +stamp="$(( RECENT_BASE + 4 * 86400 ))" +GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$BOMB" add -A >/dev/null 2>&1 +GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$BOMB" commit -q -m "bulk sweep: 101 files" >/dev/null 2>&1 +# presence guards: the bomb commit really touches 101 files, and it is HEAD +bombfiles="$( git -C "$BOMB" show --name-only --format= HEAD 2>/dev/null | grep -c . )" +[ "$bombfiles" = 101 ] && ok "arm 7 guard: the bomb commit touches 101 files (> the 100-file rule)" \ + || no "arm 7 guard: the bomb commit touches $bombfiles files, not 101 — the arm below would be vacuous" +R7="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$BOMB" --rank-by=churn-decay --no-cache 2>/dev/null )" +r7_recent="$( printf '%s' "$R7" | grep -oE ']*>' | head -1 )" +[ -n "$r7_recent" ] && ok "arm 7 guard: the bomb fixture emits a block ($r7_recent)" \ + || no "arm 7 guard: no block on the bomb fixture — nothing below can be asserted" +printf '%s' "$r7_recent" | grep -q 'merge_bombs_skipped="1"' \ + && ok "arm 7a: discloses merge_bombs_skipped=\"1\" — the 101-file commit was skipped and SAYS so" \ + || no "arm 7a: does not carry merge_bombs_skipped=\"1\" (got: $r7_recent)" +printf '%s' "$R7" | grep -q ' row — the merge-bomb rule did not skip the commit, so the counter measures nothing" \ + || ok "arm 7b: the bulk-only files are absent from the rows (the skipped commit contributed nothing)" +printf '%s' "$r7_recent" | grep -q 'of="1"' \ + && ok "arm 7c: of=\"1\" — only small.py was touched by a COUNTED commit" \ + || no "arm 7c: of= is not 1 (got: $r7_recent)" +printf '%s' "$R6" | grep -oE ']*>' | head -1 | grep -q 'merge_bombs_skipped="0"' \ + && ok "arm 7d: a window with no merge bomb says merge_bombs_skipped=\"0\" (always emitted; absence is never ambiguous)" \ + || no "arm 7d: the plain fixture's lacks merge_bombs_skipped=\"0\" (got: $( printf '%s' "$R6" | grep -oE ']*>' | head -1 ))" +# The threshold counts INDEXED files — the resolved fileIds the crawl holds, never the commit's raw +# --name-only count — and both legends used to say "100 files", which reads as the latter. A 120-.txt + +# 1-.py commit is NOT skipped by this rule, so the prose was describing a different rule from the code's. +printf '%s' "$R7" | grep -q 'merge_bombs_skipped= ' && printf '%s' "$R7" | grep -q '100 INDEXED files' \ + && ok "arm 7e: the full legend defines merge_bombs_skipped= and states the 100-INDEXED-file threshold" \ + || no "arm 7e: the full legend does not define merge_bombs_skipped= with its INDEXED-file threshold" +R7c="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$BOMB" --rank-by=churn-decay --no-cache --legend=compact 2>/dev/null )" +printf '%s' "$R7c" | grep -q 'merge_bombs_skipped=N' && printf '%s' "$R7c" | grep -q '100 INDEXED files' \ + && ok "arm 7f: the compact legend defines merge_bombs_skipped=N with the 100-INDEXED-file threshold" \ + || no "arm 7f: the compact legend does not define merge_bombs_skipped= (legend: $( printf '%s' "$R7c" | grep -oE '' | head -c 300 ))" +if command -v xmllint >/dev/null 2>&1; then + printf '%s' "$R7" | xmllint --noout - 2>/dev/null \ + && ok "arm 7g: the bomb fixture's output is well-formed XML" \ + || no "arm 7g: xmllint rejected the bomb fixture's output" +fi +# 7h: a window whose EVERY commit is a merge bomb (a shallow clone of a large tree is exactly this shape — llvm-project at +# depth 1 is one 183,835-file commit) must still print the block, with zero rows and the count: an ABSENT block reads as +# "no history mined", which is a different fact, and the disclosure arm 7a exists for would vanish on the one run that +# needs it most. Byte-free elsewhere is kept: a corpus with no git at all still prints no block (the goldens). +ONLYBOMB="$WORK/onlybomb"; mkdir -p "$ONLYBOMB/bulk" +git -C "$ONLYBOMB" init -q 2>/dev/null +git -C "$ONLYBOMB" config user.email rw@example.invalid +git -C "$ONLYBOMB" config user.name ripwire-gate +i=0 +while [ "$i" -le 100 ]; do + printf 'def bulk_%03d():\n return %d\n' "$i" "$i" > "$ONLYBOMB/bulk/b$( printf '%03d' "$i" ).py" + i=$(( i + 1 )) +done +stamp="$(( RECENT_BASE + 5 * 86400 ))" +GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$ONLYBOMB" add -A >/dev/null 2>&1 +GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$ONLYBOMB" commit -q -m "one bulk commit" >/dev/null 2>&1 +[ "$( git -C "$ONLYBOMB" rev-list --count HEAD 2>/dev/null )" = 1 ] && ok "arm 7h guard: the only-bomb fixture has exactly one commit" \ + || no "arm 7h guard: the only-bomb fixture does not have exactly one commit" +R7h="$( perl -e 'alarm 30; exec @ARGV' "$BIN" "$ONLYBOMB" --rank-by=churn-decay --no-cache 2>/dev/null )" +printf '%s' "$R7h" | grep -q '' \ + && ok "arm 7h: a window whose only commit was skipped still prints — zero rows, and the reason" \ + || no "arm 7h: the only-bomb window prints no block (got: '$( printf '%s' "$R7h" | grep -oE ']*>' | head -1 )') — the disclosure vanished with the rows" + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail diff --git a/test/docscommandscheck.sh b/test/docscommandscheck.sh index 19282304..c98e875a 100755 --- a/test/docscommandscheck.sh +++ b/test/docscommandscheck.sh @@ -41,6 +41,12 @@ # once (both derive from parse_help), so the sets stay equal and (B) stays green while the flag # is absent from the shipped document. This arm imports parse_help and judges it directly, # against a pinned ledger of the losses that already exist plus synthetic seam fixtures +# (J) CONTENTS LINKS RESOLVE — every generated `](#anchor)` must name a heading anchor the RENDERER +# mints. The anchor derivation substituted punctuation where the renderer deletes it, so all 169 +# links in the table of contents were dead and no arm above could see it: (B) compares flag name +# sets and (G) compares bytes, and a document can be byte-reproducible with every link broken. +# The rule is restated here rather than imported from the generator — importing it would agree +# with the generator's own mistake — and a control breaks one fragment in a copy # (I) SMELL-NAME FINDABILITY — the help entries that measure Fowler's Shotgun Surgery must NAME it, # because COMMANDS.md is generated from --help and a hand-edit of the document alone is undone # by the next regen; a control strips the name from a copy of the same text and must go red @@ -614,5 +620,64 @@ else ok "(I) mutation control — stripping the name from a copy of --help loses both entries (the arm can fail)" fi +# ── (J) CONTENTS LINKS RESOLVE — to the anchor the RENDERER mints, not the one we would prefer ───── +# WHY. The table of contents is generated: one `[`--flag`](#anchor)` per entry, derived from the flag's +# spec. The derivation replaced every RUN of non-alphanumerics with a hyphen (`--in=DIR` -> `#in-dir`) +# where the renderer's own rule DELETES that punctuation instead of substituting it (`#--indir`), so +# all 169 links pointed at nothing and had done since this document was first generated. CodeRabbit +# reported it on the --in row; markdownlint's MD051 had been reporting it 28 times on one line. +# Nothing in the suite had ever looked at a link, which is how a whole table of contents came to be +# dead inside a gated, byte-reproduced document. +# +# The renderer's rule is stated HERE rather than imported from the generator, deliberately: a gate that +# asks the generator what the anchor should be agrees with whatever the generator says (the +# self-referential-baseline trap). Fenced sample output is skipped — a `###` line inside a ``` block is +# code and mints no anchor, and this document's --recall sample quotes its own headings. +anchorAudit() { # $1 = markdown file -> " [sample...]" + python3 - "$1" <<'ANCHORPY' +import collections, re, sys + +def slug( text ): # GitHub's heading slug: lower-case, drop every + s = re.sub( r'[^\w\- ]', '', text.strip().lower() ) # char that is not word/-/space, spaces -> '-' + return s.replace( ' ', '-' ) + +lines, fence, heads = open( sys.argv[ 1 ] ).read().split( '\n' ), False, [] +for line in lines: + if line.startswith( '```' ): + fence = not fence + elif not fence and re.match( r'^#{1,6} ', line ): + heads.append( line.split( ' ', 1 )[ 1 ] ) +seen, anchors = collections.Counter(), set() +for h in heads: + b = slug( h ) + seen[ b ] += 1 + anchors.add( b if seen[ b ] == 1 else '%s-%d' % ( b, seen[ b ] - 1 ) ) +frags = re.findall( r'\]\(#([^)]+)\)', '\n'.join( lines ) ) +bad = sorted( { f for f in frags if f not in anchors } ) +print( '%d %d %s' % ( len( frags ), len( bad ), ' '.join( bad[ :4 ] ) ) ) +ANCHORPY +} +aud="$( anchorAudit "$DOC" )" +aLinks="$( printf '%s' "$aud" | cut -d' ' -f1 )"; aBad="$( printf '%s' "$aud" | cut -d' ' -f2 )" +if [ -z "$aLinks" ] || [ "$aLinks" -lt 100 ]; then + no "(J) the audit found only '$aLinks' contents links in docs/COMMANDS.md — the arm has no target (vacuous)" +elif [ "$aBad" = 0 ]; then + ok "(J) all $aLinks contents links resolve to an anchor the renderer mints" +else + no "(J) $aBad of $aLinks contents links resolve to no heading: $( printf '%s' "$aud" | cut -d' ' -f3- ) — fix anchor_of in docs/docs_commands_build.py and regenerate" +fi +# mutation control: break ONE fragment in a copy, and the same audit must report exactly one dead link +cp "$DOC" "$TMP/anchor.md" +python3 - "$TMP/anchor.md" <<'ANCHORMUT' +import sys +p = sys.argv[ 1 ] +s = open( p ).read().replace( '](#', '](#zzz-no-such-heading-', 1 ) +open( p, 'w' ).write( s ) +ANCHORMUT +mAud="$( anchorAudit "$TMP/anchor.md" )" +[ "$( printf '%s' "$mAud" | cut -d' ' -f2 )" = 1 ] \ + && ok "(J) mutation control — one broken fragment in a copy audits as exactly one dead link" \ + || no "(J) mutation control is inert: a copy with one broken fragment audits as '$mAud'" + if [ "$fail" = 0 ]; then printf 'ALL PASS\n'; else printf 'FAILURES ABOVE\n'; fi exit "$fail" diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index 8bf5d837..9ed4d461 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -161,7 +161,7 @@ TABLE = { ( "src/verbs_report.h", "row" ): ( 5, "safe", "row[96] + row[192], runSkipped's two row emitters (§L1). row[96] at the drop row: '\" why=\"%s\" bytes=\"%llu\" ext=\"' where %s is the CLOSED vocabulary {oversize, excluded, unsupported-ext, ignored, ignored-dir, nest-refused, escaped-root} (15 B longest, still unsupported-ext; escaped-root is 12) = ~60 B. row[192] at the parse-health row: three %s from the closed why= vocabulary (31 B for the joined 'degraded-parse,minified-suspect'), one %u, two %.3f of ratios that are <=1.0 by construction (errBytes sums DISJOINT top-most ERROR spans, ws sample is its own denominator) and 14 B even if a future edit broke that, one %u = ~131 B. Both p= values are written by escapeXml OUTSIDE the buffer." ), # ── src/mcpverbs.h ─────────────────────────────────────────────────────────────────────────────────── ( "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/pageview.h", "buf + written" ): ( 3, "safe", "THREE appends into the TAIL of a caller buffer, all bounded by (bufCap - written) and guarded by a written-versus-bufCap test, and all three sized against kPageDisclosureCap by construction. (1) pageDisclosure's H8 floor marker (capture-audit L4): the %s is syn.floor, one of TWO fixed literals. (2,3) pagingDisclosure's two dialect arms (C1-b, 2026-09-13): the paging half is written after an OPTIONAL leading total=, so an element that already spells its total under its own name (the map's ) can skip it — the arms interpolate counts and the syntax table's boolean literals only, nothing user-supplied and nothing escaped." ), # ── src/packtask.h ─────────────────────────────────────────────────────────────────────────────────── ( "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 ────────────────────────────────────────────────────────────────────────────────── @@ -260,7 +260,7 @@ NUMERIC_ONLY = { ( "src/serialize.h", "nb" ): 1, # row 6 (2026-09-12): appendCalleeNameRow's `"\" l=\"{}\"/>"` buffer went with the merge ( "src/serialize.h", "precAttr" ): 1, ( "src/serialize.h", "rankAttr" ): 1, - ( "src/serialize.h", "rc" ): 2, + ( "src/serialize.h", "rc" ): 1, ( "src/serialize.h", "rootsAttr" ): 1, ( "src/serialize.h", "sh" ): 1, ( "src/serialize.h", "skippedAttr" ): 1, @@ -467,6 +467,18 @@ if not bad: # because formatTo's SILENT truncation is precisely what was not saving the old bound — it writes # at most cap-1 and its return is not read there, so an overrun would have dropped the closing # quote of truncated=" and shipped a malformed root. Same buffer, same one call, same one row. +# 2026-09-14 (MERGE of lane/recent-scope with #214, and the reason this pin is re-derived rather +# than resolved): BOTH sides moved this pin from the SAME base 49220049 — mentions 322 -> 324, +# calls/sites 218 -> 219 — for DIFFERENT reasons (this lane: --in=DIR's scoped-recent emitters, +# pageview.h's `buf + written` tail going one entry to three and serialize.h's `rc` re-derived; +# #214: prcontext's est-unmeasured label and the tail[256] -> tail[320] growth). Identical text on +# both sides means git auto-merges the line CLEAN at 324/219/219, and that value describes NEITHER +# tree: the merged population carries both sets of additions. Re-derived from the merged source by +# the gate's own arithmetic — mentions 326, calls 220, sites 220 — and cross-checked against the two +# deltas summing (322+2+2, 218+1+1). rows/widthforms UNCHANGED at 92/0, and (S1)/(S2)/(S1b) pass on +# the merged tree, so every one of the 220 sites is classified, no TABLE row is stale, and no new row +# is owed: only the counts moved. A pin two lanes raised to the same number from the same base is the +# case where neither side is correct (memory: trap-neither-side-of-a-pin-conflict-is-correct). # 2026-09-14 (MERGE of lane/sc-legend with main at 0b118ac1 — NEITHER SIDE'S NUMBER WAS RIGHT): # mentions 326, calls 220, sites 220, rows 94. Both sides of this line said 324, and both were # correct about their own tree: this lane re-derived 323 -> 324 for the chooseExpandServe split, @@ -479,7 +491,17 @@ if not bad: # accounted for and every row's multiplicity already matches source — the two new TABLE rows of # this lane (fileOpen[200], bundleOpen[200]), `main.cpp open` at 2, and packtask.h's `open` at 3 # among them. Only the totals line moved. -EXPECTED = { "mentions": 326, "calls": 220, "sites": 220, "rows": 94, "widthforms": 0 } +# 2026-09-14 (MERGE with #215, and the THIRD lane to raise this pin to the same number from a +# different base): main before #215 stood at 324/219/219; this lane derived 326/220/220 from it and +# #215 independently derived 326/220/220 from it, each +2 mentions and +1 call/site for unrelated +# reasons — so for the third time the two sides agree on a value that describes NEITHER merged tree, +# and git auto-merges everything except the rows field. #215 landed first, so its delta is in the +# baseline and this lane's has to be added on top: 324+2+2 = 328 mentions, 219+1+1 = 221 calls and +# sites. Re-derived from the merged source by the gate itself rather than accepted as arithmetic, and +# (S1) confirms the MEMBER SET behind the numbers: 221 sites over 35 hand-classified TABLE rows (33 +# plus #215's two new ones) and 59 derived NUMERIC_ONLY rows, with (S2) reporting no stale row and +# (S1b) nothing breaching. rows is 94 from #215's two additions, which this lane does not touch. +EXPECTED = { "mentions": 328, "calls": 221, "sites": 221, "rows": 94, "widthforms": 0 } # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the # H9 ` max_tokens="%d"` ceiling disclosure: a single %d of a caller-supplied INTEGER, no %s, diff --git a/test/legendcoveragecheck.sh b/test/legendcoveragecheck.sh index 41841895..4dd78443 100755 --- a/test/legendcoveragecheck.sh +++ b/test/legendcoveragecheck.sh @@ -135,15 +135,24 @@ ROSTER = [ # Rung zero drops the confidence=/margin_pct=/budget_tokens= clause and the r=/tail clause to buy the # bytes back, which is the right trade (METHODOLOGY §9: inside the budget beats over it) and was # entirely silent: the attributes stayed on the root with nothing defining them and nothing saying so. - # 1300 sits well inside the measured rung-zero band on this tree (rung zero fires from ~1100 to ~1340), - # so the row keeps exercising the rung under ordinary drift; if the band moves off it, the row degrades - # into an ordinary budgeted --for and arm (A) still holds — it just stops proving this particular thing. - ("for-budgeted", [SMALL, "--for=rank symbols by pagerank", "--token-budget=1300"]), + # THE BUDGET IS 900, AND THE OLD 1300 WAS A KNIFE-EDGE. The band recorded here was "~1100 to ~1340", + # measured only on a developer worktree — and the rung's top edge is a function of the CHECKOUT PATH + # LENGTH, because the document charges its own `root="…"` bytes. Measured on this tree at two roots: + # a 38-character root fires rung zero at 1287 and NOT at 1288; a 137-character root fires at 1323 and not + # at 1324 (bisected, same commit and binary). CI checks the tree out into the runner's work directory, + # which is 33 characters on the Linux + # runners and 34 on the macOS ones (this file may not spell either path: ripwirepubliccheck arm 2 refuses + # an absolute home path in a tracked file, and it is right to, since it cannot tell a runner's home from + # a person's). So 1300 was INSIDE the band on a long local path and OUTSIDE it on every CI runner — green + # here, red on three shards. The band's floor is flat: from 100 up to the top edge the note's text and arm + # (F)'s census are byte-identical, so 900 is mid-rung by ~390 tokens of headroom rather than a widened + # tolerance, and arm (E) now GATES that headroom instead of trusting a comment (see MARGIN below). + ("for-budgeted", [SMALL, "--for=rank symbols by pagerank", "--token-budget=900"]), # …and the same rung in the COMPACT DIALECT, which has its own legend strings for every clause the rung # drops (kForCompactConfidenceClause, kForFileTailLegendCompact) and had no roster row at all. A dialect # with no row is a dialect where the ratchet cannot see a gap, and this one carries `schema=` that the # default dialect does not emit. - ("for-budgeted-compact", [SMALL, "--legend=compact", "--for=rank symbols by pagerank", "--token-budget=1300"]), + ("for-budgeted-compact", [SMALL, "--legend=compact", "--for=rank symbols by pagerank", "--token-budget=900"]), # …and the FILE PAGE (L-W, forpage.h): --for --limit=N answers with its own root and rows, a # first screen no other row here reaches; both dialects, since the compact one carries its own short legend. ("for-page", [SMALL, "--for=rank symbols by pagerank", "--limit=10"]), @@ -362,7 +371,19 @@ import subprocess, re, sys, os BIN, ROOT, TMP = sys.argv[1:4] SMALL = os.path.join( ROOT, "src" ) QUERY = "--for=rank symbols by pagerank" -TIGHT, WIDE = "--token-budget=1300", "--token-budget=8000" +# TIGHT is MID-RUNG, and MARGIN is what proves it. The rung-zero band's top edge moves with the length of +# the checkout path, because the document charges its own `root="…"` bytes. Bisected on this tree, same commit +# and binary, only the root differing: a 38-character root fires rung zero at 1287 and NOT at 1288; a +# 137-character root fires at 1323 and not at 1324. The old TIGHT of 1300 sat BETWEEN those two numbers, so +# this arm was green on a developer worktree and red on every CI runner (CI checks out at 33-34 characters). +# A budget that only fires on the machine that chose it is not a probe. TIGHT=900 sits 387 tokens below the +# SHORTEST measured edge, and the band is flat beneath it — from 100 up to the edge the note's text and arm +# (F)'s census do not change — so this is mid-rung, not a tolerance. Both edges move when src/ changes (this +# lane re-measured them across a merge that added a 385-line file, and the short one moved by a single token), +# which is the other reason the headroom is gated below rather than trusted here. +# MARGIN re-runs the same query 200 tokens ABOVE TIGHT and requires the note there too, which turns "the +# probe has headroom" from a claim in this comment into a row that fails when it stops being true. +TIGHT, MARGIN, WIDE = "--token-budget=900", "--token-budget=1100", "--token-budget=8000" CORE = { "p", "n", "t", "id", "l", "k", "c" } # The (F) floor: keys whose ONLY definition on the budgeted document is the rung-zero note, AND whose attribute @@ -414,6 +435,13 @@ for dialect, extra in ( ( "default", [] ), ( "compact", [ "--legend=compact" ] ) if mw: bad.append( f"{dialect}: the dropped-legend note is STILL THERE at {WIDE} — 'a wider token-budget' is not wide enough to be the control, so nothing below is evidence" ) continue + # HEADROOM, gated rather than asserted in a comment: the same query 200 tokens above TIGHT must still + # climb rung zero. This is the arm that would have caught the old 1300 on the machine that chose it — + # a probe one step from the rung's top edge passes locally and fails wherever the charged bytes differ, + # and the checkout path length alone is enough to move them. + if not NOTE.search( legendOf( doc( [ SMALL, QUERY, MARGIN ] + extra ) ) ): + bad.append( f"{dialect}: the note is present at {TIGHT} but ABSENT at {MARGIN} — the probe is within 200 tokens of the rung's TOP edge, so it no longer fires on a document any smaller (a shorter checkout path is enough). Re-anchor TIGHT to the middle of the measured band, do not widen a tolerance" ) + continue names = sorted( set( NAME.findall( mt.group( 0 ) ) ) ) if not names: bad.append( f"{dialect}: the note names no attribute at all ({mt.group(0)[:80]}…) — it cannot tell a reader which definition went" ) diff --git a/test/mcpcontractcheck.sh b/test/mcpcontractcheck.sh index dd14358e..ff90a670 100755 --- a/test/mcpcontractcheck.sh +++ b/test/mcpcontractcheck.sh @@ -360,6 +360,9 @@ TWIN = { # the CLI report's is 8: the payload is machine-read and has always served every row, so limit there is # relief for a caller who wants less, never a new cut. "--flags": "flags", "--situ": "situational_awareness", + # 2026-09-12 (C1-b): --in=DIR joined the paging set for its page (--offset=N). The MCP server + # exposes no churn ranker at all (no rank_by argument on any tool), so there is nothing for it to twin: CLI-only. + "--in": "", # L-W (2026-09-12, forpage.h): --for joined the paging set for its FILE PAGE (--limit/--offset select the # one-row-per-file widening document); its twin takes the same limit/offset through mcpPageArgs. "--for": "for", diff --git a/test/nextverbcheck.sh b/test/nextverbcheck.sh index 78cea7da..f7412346 100755 --- a/test/nextverbcheck.sh +++ b/test/nextverbcheck.sh @@ -179,5 +179,70 @@ rrun --help-task='where is the rot in code I did not write' >"$TMP/ht2" if [ -z "$( nexts "$TMP/ht2" )" ]; then ok "help-task non---for recommendation carries no next=" else no "help-task non---for recommendation carries a next=: $( nexts "$TMP/ht2" )"; fi +echo '=== (9) next= quoting: a tilde is shell-special only WORD-INITIALLY, and the attribute is XML ===' +# CodeRabbit on #212 (review 5192382995), against the captured showcase: the scoped block's next= read +# next="--rank-by=churn-decay --since='HEAD~1' …" +# nextFlag quoted HEAD~1 because the value contains `~`, and then the XML escaper turned the single quotes +# into ' — which a reader pasting the raw text out of a Markdown capture does not decode. The attribute +# exists to be pasted, so that is the whole of its job lost, on the most ordinary git revision spelling there +# is. Tilde expansion is defined for a WORD-INITIAL `~` (and the ~user form) and for nothing else, so HEAD~1 +# was never special and never needed the quotes. +# +# THE TWO ROWS ARE EACH OTHER'S CONTROL, which is why neither needs a planted mutation: a rule that quoted +# nothing would red the ~tmp row, and a rule that quoted everything (the defect) reds the HEAD~1 row. Only the +# position-aware rule passes both. +QREPO="$TMP/qrepo"; mkdir -p "$QREPO/sub" +( cd "$QREPO" && git init -q && git config user.email "t@example.com" && git config user.name "t" \ + && printf 'int qa(){return 1;}\n' > sub/a.cpp && printf 'int qb(){return 2;}\n' > sub/b.cpp \ + && git add -A && git commit -q -m one \ + && printf 'int qa(){return 11;}\n' > sub/a.cpp && printf 'int qb(){return 22;}\n' > sub/b.cpp \ + && git add -A && git commit -q -m two ) || { echo "(9) qrepo setup failed"; exit 2; } + +qnext(){ "$BIN" "$QREPO" --rank-by=churn-decay --since="HEAD~1" --in=sub --limit=1 "$@" 2>/dev/null \ + | grep -o ']*>' | head -1 | grep -o 'next="[^"]*"' | head -1; } + +q1="$( qnext )" +if [ -z "$q1" ]; then no "(9) the scoped block carried no next= at all — the fixture stopped capping, so neither row below is evidence" +else + case "$q1" in + *"--since=HEAD~1"*) ok "(9) a non-word-initial ~ is NOT quoted: next= carries --since=HEAD~1 bare, so it pastes from raw Markdown" ;; + *) no "(9) next= does not carry a bare --since=HEAD~1: $q1" ;; + esac + case "$q1" in + *"'"*) no "(9) next= still carries ' — the value was quoted and the XML escaper turned the quotes into entities no shell decodes: $q1" ;; + *) ok "(9) next= carries no ' entity — nothing in this invocation needed quoting" ;; + esac +fi + +# THE RULE, because this arm used to assert the opposite and explain it wrongly: tilde expansion applies to a +# word whose FIRST character is `~`. In `--exclude=~tmp` the word begins with `--exclude=`, so no shell expands +# it — an argument is not an assignment. Measured on this machine: `sh -c 'p ~root'` -> /var/root, while +# `sh -c 'p --exclude=~root'` -> the literal `--exclude=~root` (sh, bash and zsh alike). The gate previously +# pinned `--exclude='~tmp'` and called the quotes "the correct answer here", which would have +# defended the corrupted argument against anyone who fixed it. +q2="$( qnext --exclude='~tmp' )" +if [ -z "$q2" ]; then no "(9) the --exclude arm produced no next= — the tilde row below proves nothing" +else + case "$q2" in + *"--exclude=~tmp"*) ok "(9) a ~ after --flag= is NOT quoted: next= carries --exclude=~tmp bare, which is what a shell passes through" ;; + *) no "(9) next= does not carry a bare --exclude=~tmp — the value was quoted for an expansion no shell performs: $q2" ;; + esac + case "$q2" in + *"'"*) no "(9) next= still carries ' — quoting a value no shell would expand, and the XML escaper then made it an entity no shell decodes: $q2" ;; + *) ok "(9) the --exclude=~tmp invocation carries no ' entity at all" ;; + esac +fi + +# MUTATION CONTROL for the row above: narrowing the tilde case must not have disabled quoting generally. A +# value with a space still needs it, and still gets it. +q3="$( qnext --exclude='a b' )" +if [ -z "$q3" ]; then no "(9) the space-value arm produced no next= — the control below proves nothing" +else + case "$q3" in + *"--exclude='a b'"*) ok "(9) control: a value a shell WOULD split is still quoted (--exclude='a b'), so only the tilde case narrowed" ;; + *) no "(9) control: a space-bearing value lost its quoting — the fix disabled quoting instead of narrowing it: $q3" ;; + esac +fi + [ "$fail" -eq 0 ] && echo 'ALL PASS' || echo 'FAILURES ABOVE' exit "$fail" diff --git a/test/pagingsweepcheck.sh b/test/pagingsweepcheck.sh index ad2f3815..c280e82f 100755 --- a/test/pagingsweepcheck.sh +++ b/test/pagingsweepcheck.sh @@ -710,6 +710,11 @@ TABLE = { # shown=/total=/capped= facts in its section headers. Parsing a root element out of it would fail for a # reason that has nothing to do with paging, so it is checked as prose below instead. "--situ": ( [ "--situ=src/situ.h" ], "PROSE" ), + # 2026-09-12 (C1-b): --in=DIR pages the block of --rank-by=churn-decay — a CHILD of the map's + # root, which carries no window of its own (the global block and the symbol stub are not paged), so the + # root is uncut by construction and the "cut nothing ⇒ quintet absent" branch applies; the child's own capped="1" + # + next= page is test/recentscopecheck.sh's arm 3. + "--in": ( [ "--rank-by=churn-decay", "--in=src" ], None ), # L-W (2026-09-12, forpage.h): --for joins for its FILE PAGE — --limit/--offset select a document of # one row per file, NOT a window over the bundle. The bare --for root () is therefore uncut by # construction and carries no quintet; the page itself (test/forwidencheck.sh) carries the full quintet. diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 76486d61..ea99540b 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -9,7 +9,7 @@ pattern 0 52232edac49e9ec2f7c7a1717ab4a3b208e29e4ad550b17bd92bae4e13f9d4dd e3b0c callers 0 c8317dc738f05616369995c0a0c03a6c12fdea8d2a2c5b7d182156a4de76cdc6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 impact 0 4009dd7227dabc63a96e6318e77c6f704f25b7c2a31295fa80510b1df48b6e85 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 clones 0 69e768ecb0be2645c1848a99a3b6f541a0a37dbcfb7383d96a5cc90c636fe792 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help 0 aee3ae7d926954108ded89973f359725523423877a55c2c1db3a5909d6740d0f e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help 0 933da452b605799455b582aec8d73c5497dcc35eba574d01bc430a9a2ab94ab0 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 expand 0 f06c70abc1d5c8a941bd340adc08173232e83790ce024a14d20e1bcba5d2808d e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 callees 0 69470d30c1abfb98cebbabf5d62f502822cd8a0cb44dee8de42b2ba3a6349ec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 around 0 1f3efff7030b4a0a13e44736476bb920c3ae911559dd940b728e8ef5d985dc82 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -38,5 +38,5 @@ safe_delete 0 b06980d52e4991e57563059d8be986bb4614779e20ccfd3790aac1ee076d2512 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 ab9dee52240f70055fa4d82d6b928ef52f4f5781c6a80ee39edb49805b892719 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 87024074d340a6ac2d44f3a70db5c99d037446490e042517ab24b1bd86182429 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 2de8db3ca3461c278495e29289d5f62cbe46087bef57b266fcc28e595fda1320 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/recentscopecheck.sh b/test/recentscopecheck.sh new file mode 100755 index 00000000..fba68b36 --- /dev/null +++ b/test/recentscopecheck.sh @@ -0,0 +1,761 @@ +#!/usr/bin/env bash +# recentscopecheck.sh — gate for `--rank-by=churn-decay --in=DIR` (C1-b, 2026-09-12): the directory-scoped +# recent-changes block, ADDITIVE to the global one, with the symbol map collapsed to a counted stub. +# +# WHY THIS GATE EXISTS. "What changed recently in DIR?" is six of the thirty head-to-head questions, and the +# verb answered it with a whole-repo symbol map (196 KB of a 237 KB answer on the reference corpus) plus ONE +# global block that a directory with more than 40 recently-touched files never fits into. +# C1-b adds `--in=DIR`: the global block stays byte-identical (three of the six golds sit OUTSIDE the named +# directory and complete only through it), a second block follows it with DIR's files +# only — p= root-relative, exactly as the global block spells them, because a sub-root-relative spelling +# missed every held-out gold (0/30 raw vs 19/30 prefixed) — cut at 40 rows with capped="1" and a pasteable +# next= that pages by --offset=N, and the symbol map the caller did not ask for becomes a disclosed stub +# (docs/METHODOLOGY.md §9.3: a disclosed cut is still terminal). +# +# Arms: +# 0 fixture guards: 53 commits; the bare run emits . +# 1 --in=db: exit 0; a SECOND block follows the +# global one; every in it starts with "db/" (root-relative, the global block's spelling). +# 2 the GLOBAL block is byte-identical to the run without --in= (additive, never replacing). +# 3 paging: next= is `--rank-by=churn-decay --in=db --offset=40`; the page returns the 5 rows the first +# page did not, NO overlap, union == git's own file list for db/; the oldest db file (position 45) +# is on page 2 and not on page 1; the pasted next= (shlex-split) reproduces page 2 byte-for-byte; +# --limit=10 windows the page and next= carries --limit=10 --offset=10. +# 4 a gold OUTSIDE db (gold_outside.py, touched by HEAD) stays FIRST in the global block and is absent +# from the scoped one. +# 5 the stub: replaces the groups; +# N equals the un-stubbed map's shown= AND its row count; the header's own shown= reads 0. +# 6 refusals (exit 1, empty stdout, a message that names the remedy): --in= on --rank-by=churn, on +# --hotspots, alone, with --json, with --top-k=0; DIR missing, DIR a file, DIR absolute, DIR with ".."; +# and (6l/6m/6n) the DISPATCH-voided class — a report verb that answers before the default map ever +# renders, which cli.h cannot see because it does not know the winner. +# 7 a DIR with a space and a DIR starting with '-' work; a trailing slash normalises; plus a mutation +# control that proves the two "carries no capped=/next=" assertions can actually SEE those attributes. +# 8 merge_bombs_skipped= rides BOTH blocks. +# 9 determinism (two runs byte-identical), well-formed XML, both legends define scope= and the stub. +# 10 next= names THIS run's corpus and window, or says nothing at all past 120 bytes. +# 11 the scoped block's ABSENCE rule is the global block's: absent ⇒ no history mined. +# 12 the crawl CEILING rides next= too: a hint emitted under --max-file-size=N replays N, so the page it +# names is a page of the same corpus (it dropped the flag and landed on of="3" against of="2"). +# 13 "history was mined" is propagated, not inferred from the rows: a window whose only commit touched no +# INDEXED file prints n="0", where it used to print no block and read as "no history mined". +# +# RED against the pre-change binary: `--in=` is an unknown flag, so every arm below fails. +# +# Usage: RIPWIRE_BIN=build/ripwire bash test/recentscopecheck.sh | RIPWIRE_BIN=asan/ripwire bash … + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v git >/dev/null 2>&1 || { echo "recentscopecheck: git is required"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "recentscopecheck: python3 is required (shlex for the pasted next=)"; exit 2; } +# A MISSING TOOL IS AN ENVIRONMENT, NOT A REGRESSION (CodeRabbit, review 5195637558). Both of these were +# handled below the line instead of here, and each failed in a way that blamed the binary: without perl, +# run() returned "" and nearly every arm reported the tool as broken; without xmllint, arm 9b printed FAIL +# and test/regression.sh reported this gate as a product regression for a tool the machine never had. +# Exit 2 is the house cannot-conclude, and it names WHICH tool so the reader does not have to guess. +command -v perl >/dev/null 2>&1 || { echo "recentscopecheck: perl is required (the per-run 60 s alarm in run())"; exit 2; } +command -v xmllint >/dev/null 2>&1 || { echo "recentscopecheck: xmllint is required (arms 9b/9c verify well-formedness)"; exit 2; } + +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +echo "recentscopecheck: BIN=$BIN" + +# ── the fixture: one commit per file, ages distinct, HEAD's clock the anchor ───────────────────────── +# db/f00.py .. db/f44.py — 45 files, f00 committed FIRST (oldest), f44 last: age ascending puts f44 at row 1 +# and f00 at row 45, so the 40-row page cuts f00..f04 onto page 2. +# util/u0..u2.py — 3 files, older than every db file. +# "my dir"/m0,m1.py — a directory with a space. +# -dash/d0,d1.py — a directory whose name starts with '-'. +# gold_outside.py — touched by HEAD itself: the newest file in the repo, OUTSIDE db/. +REPO="$WORK/repo"; mkdir -p "$REPO/db" "$REPO/util" "$REPO/my dir" "$REPO/-dash" +git -C "$REPO" init -q 2>/dev/null +git -C "$REPO" config user.email rw@example.invalid +git -C "$REPO" config user.name ripwire-gate +NOW="$( date +%s )" +BASE="$(( NOW - 400 * 86400 ))" +n=0 +commitOne() # $1 = path (relative), $2 = symbol name +{ + n=$(( n + 1 )) + local stamp="$(( BASE + n * 86400 ))" + printf 'def %s():\n return %d\n' "$2" "$n" > "$REPO/$1" + GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$REPO" add -A >/dev/null 2>&1 + GIT_AUTHOR_DATE="$stamp +0000" GIT_COMMITTER_DATE="$stamp +0000" git -C "$REPO" commit -q -m "touch $1" >/dev/null 2>&1 +} +for i in 0 1 2; do commitOne "util/u$i.py" "util_$i"; done +for i in 0 1; do commitOne "my dir/m$i.py" "mdir_$i"; done +for i in 0 1; do commitOne "-dash/d$i.py" "dash_$i"; done +i=0 +while [ "$i" -le 44 ]; do commitOne "db/f$( printf '%02d' "$i" ).py" "db_$i"; i=$(( i + 1 )); done +commitOne "gold_outside.py" "gold_outside" + +# PRESENCE GUARDS (CONTRIBUTING §2): the arms are meaningless if the fixture has no history, or if the bare +# verb does not emit the global block the scoped one is measured against. +[ "$( git -C "$REPO" rev-list --count HEAD 2>/dev/null )" = 53 ] \ + && ok "arm 0a: fixture has 53 commits" \ + || no "arm 0a: fixture does not have 53 commits (got $( git -C "$REPO" rev-list --count HEAD 2>/dev/null ))" +[ "$( git -C "$REPO" ls-files db | grep -c . )" = 45 ] \ + && ok "arm 0b: db/ holds 45 tracked files (> the 40-row page)" \ + || no "arm 0b: db/ does not hold 45 tracked files" + +# EVERY RUN OF THE BINARY IN THIS GATE GOES THROUGH HERE, and that is a population rule, not a style one +# (CodeRabbit, review of #212, on the continuation replay below). A hang in an unbounded probe does not fail +# an arm: it burns the whole gate budget and pargates kills the gate with NO verdict row, which reads as a +# broken gate rather than a hung product. The review named one site; swept, this file had NINE that bypassed +# this helper — refuses(), composes(), the replayed next=, the derived shaper loop, --top-k=0, the case-fold +# and empty-value arms, and both runs inside wsFor() — two of them added by the very commits that fixed the +# earlier findings. They all route through runAt (or carry their own timeout, for the python replay) now, so +# a new arm inherits the bound by using the helper every other arm uses. +runAt(){ local r="$1"; shift; perl -e 'alarm 60; exec @ARGV' "$BIN" "$r" --no-cache "$@"; } # arms 12/13 bring their own repo +run(){ runAt "$REPO" "$@"; } +# THE PASTED next=, split the way a shell would — one copy, used by arms 3h, 10d and 12d. Three inline +# copies of this python one-liner is the duplicate --exemplar would have found; it takes the repo because +# the corpus arms below are measured on fixtures of their own. +# BOUNDED like every other run here (see the sweep note above runAt): a replayed next= is a command this +# gate did not write, so it is exactly the invocation that could hang, and an unbounded subprocess.run would +# hang the whole gate with no row rather than failing this arm. +pasteNext(){ python3 -c 'import shlex,subprocess,sys +try: + sys.stdout.write(subprocess.run([sys.argv[1],sys.argv[2],"--no-cache"]+shlex.split(sys.argv[3]),capture_output=True,text=True,timeout=60).stdout) +except subprocess.TimeoutExpired: + sys.exit("pasteNext: the replayed continuation did not finish in 60s: " + sys.argv[3])' "$BIN" "$1" "$2"; } +firstBlock(){ printf '%s' "$1" | grep -oE ']*>.*' | sed -E 's#.*##' | head -1; } # the GLOBAL block, inner rows included +scopedTag(){ printf '%s' "$1" | grep -oE ' (exit 0)" \ + || no "arm 0c: bare run exit=$ec, recent tag: $( printf '%s' "$BARE" | grep -oE ']*>' | head -1 )" +printf '%s' "$BARE" | grep -q ' (got: $tag)" \ + || no "arm 1b: scoped block tag wrong or missing (got: '$tag')" +printf '%s' "$tag" | grep -q 'capped="1"' \ + && ok "arm 1c: the 45-file directory is capped=\"1\" at 40 rows" \ + || no "arm 1c: no capped=\"1\" on a 45-file directory" +gpos="$( printf '%s' "$IN" | grep -bo ' block is byte-identical with and without --in= ($( printf '%s' "$gb_bare" | wc -c | tr -d ' ' ) B)" +else + no "arm 2: the global block CHANGED under --in=:"; printf ' bare: %.200s\n in: %.200s\n' "$gb_bare" "$gb_in" +fi + +# ── arm 3: paging — next= is pasteable and the page is the exact remainder ────────────────────────── +next1="$( printf '%s' "$tag" | grep -oE 'next="[^"]*"' | sed 's/^next="//;s/"$//' )" +[ "$next1" = "--rank-by=churn-decay --in=db --offset=40" ] \ + && ok "arm 3a: next= is the page: $next1" \ + || no "arm 3a: next= is '$next1', expected '--rank-by=churn-decay --in=db --offset=40'" +P2="$( run --rank-by=churn-decay --in=db --offset=40 2>"$WORK/e3" )"; ec=$? +tag2="$( scopedTag "$P2" )" +[ "$ec" = 0 ] && printf '%s' "$tag2" | grep -q '^ (got: $tag2)" \ + || no "arm 3b: page 2 exit=$ec, tag: '$tag2'" +printf '%s' "$tag2" | grep -q 'offset="40"' \ + && ok "arm 3c: page 2 says offset=\"40\"" \ + || no "arm 3c: page 2 lacks offset=\"40\"" +# capped= compares the PAGE to the total, so a LAST page of a cut listing still says capped="1" — pageview.h +# rule 3 (M2) defines it that way on purpose, and has_more="0" is what ends a paging loop. The assertion that +# matters here is has_more="0" with NO next=: nothing further to fetch, and nothing pasteable claiming there is. +if [ -n "$tag2" ] && printf '%s' "$tag2" | grep -q 'has_more="0"' && printf '%s' "$tag2" | grep -q 'next_offset="45"' && ! printf '%s' "$tag2" | grep -qE 'next='; then + ok "arm 3d: the last page says has_more=\"0\" next_offset=\"45\" and carries no next=" +else + no "arm 3d: the last page must say has_more=\"0\" next_offset=\"45\" with no next= (got: '$tag2')" +fi +rows2="$( scopedRows "$P2" )" +overlap="$( comm -12 <( printf '%s\n' "$rows1" | sort ) <( printf '%s\n' "$rows2" | sort ) | grep -c . )" +[ -n "$rows1" ] && [ -n "$rows2" ] && [ "$overlap" = 0 ] \ + && ok "arm 3e: pages 1 and 2 share no row" \ + || no "arm 3e: $overlap rows appear on both pages (or a page is empty)" +union="$( printf '%s\n%s\n' "$rows1" "$rows2" | grep . | sort -u )" +gitdb="$( git -C "$REPO" ls-files db | sort -u )" +[ "$union" = "$gitdb" ] && ok "arm 3f: page 1 ∪ page 2 == git's own db/ file list (45 files, none dropped, none invented)" \ + || no "arm 3f: the two pages do not reproduce git's db/ list (union $( printf '%s\n' "$union" | grep -c . ), git $( printf '%s\n' "$gitdb" | grep -c . ))" +printf '%s\n' "$rows2" | grep -qx 'db/f00.py' && ! printf '%s\n' "$rows1" | grep -qx 'db/f00.py' \ + && ok "arm 3g: the oldest db file (row 45) is reachable on page 2 and absent from page 1" \ + || no "arm 3g: db/f00.py is not exactly on page 2 (page1: $( printf '%s\n' "$rows1" | grep -c 'db/f00.py' ), page2: $( printf '%s\n' "$rows2" | grep -c 'db/f00.py' ))" +# the pasted next=, split the way a shell would, reproduces page 2 byte for byte +PASTED="$( pasteNext "$REPO" "$next1" )" +[ -n "$PASTED" ] && [ "$PASTED" = "$P2" ] \ + && ok "arm 3h: the pasted next= reproduces page 2 byte-for-byte" \ + || no "arm 3h: the pasted next= does not reproduce page 2" +L10="$( run --rank-by=churn-decay --in=db --limit=10 2>/dev/null )" +tagL="$( scopedTag "$L10" )" +printf '%s' "$tagL" | grep -q '^]*/>' | head -1 )" +bareShown="$( printf '%s' "$BARE" | grep -oE ' shown=[0-9]+' | head -1 | tr -dc '0-9' )" +bareRows="$( printf '%s' "$BARE" | grep -o ' count)" \ + || no "arm 5 guard: un-stubbed shown='$bareShown' vs rows=$bareRows" +# The stub does NOT borrow the paging vocabulary: total= there would be the --top-k PAGE SIZE on a document +# whose header says symbols=N (pageview.h rule 2 reserves total= for THE total), and a shown= would drag a +# capped= with it (rule 3). It says what it is instead — stubbed="1" plus would_show=, the rows the same run +# without --in= would PRINT. +[ "$stub" = "" ] \ + && ok "arm 5a: the stub is $stub" \ + || no "arm 5a: stub is '$stub', expected " +printf '%s' "$stub" | grep -qE ' total=| shown=| capped=' \ + && no "arm 5a2: the stub must carry NO total=/shown=/capped= — that vocabulary belongs to a page (got: $stub)" \ + || ok "arm 5a2: the stub borrows no paging attribute (no total=, no shown=, no capped=)" +printf '%s' "$IN" | grep -q '"$WORK/r.out" 2>"$WORK/r.err" block of either kind is built there either — voided like the rest, not a composition. +refuses "arm 6l: --in= voided by --lint winning dispatch" "--lint" --rank-by=churn-decay --in=db --lint +refuses "arm 6m: --in= voided by --hotspots winning dispatch" "--hotspots" --rank-by=churn-decay --in=db --hotspots +refuses "arm 6n: --in= voided by --query winning dispatch" "--query" --rank-by=churn-decay --in=db --query=f00 +refuses "arm 6o: --in= voided by --map-diff" "--map-diff" --rank-by=churn-decay --in=db --map-diff +refuses "arm 6p: --in= voided by --expand" "--expand" --rank-by=churn-decay --in=db --expand=db_0 +refuses "arm 6q: --in= voided by --pack-signatures" "--pack-signatures" --rank-by=churn-decay --in=db --pack-signatures +refuses "arm 6r: --in= voided by --doctor" "--doctor" --rank-by=churn-decay --in=db --doctor +refuses "arm 6s: --in= voided by --batch" "--batch" --rank-by=churn-decay --in=db --batch=- + +# INERT IS NOT COMPETING (CodeRabbit, review of #212). firstFlagOutside answers "which set flag is not a +# ride-along", and the generic refusal then says that flag "answers instead". For --no-redact that was FALSE: +# it selects no operation, it only stops body redaction, and a scoped run serves no bodies at all (the symbol +# map is the counted stub). It is refused ahead of the generic diagnostic now, saying exactly that. Arm 6s2b is +# the control: a flag that really does emit its own answer must KEEP the generic wording, or this fix would +# have replaced one wrong sentence with another. +refuses "arm 6s2: --in= with --no-redact says INERT, not answers-instead" "has nothing to un-redact" --rank-by=churn-decay --in=db --no-redact +refuses "arm 6s2b control: a real competitor keeps answers-instead" "answers instead" --rank-by=churn-decay --in=db --external-surface +# AND THE ARM THAT WAS MISSING. Both arms above pass --in, so neither could see that the new branch had no +# --in guard at all: it refused a plain `--no-redact` run while quoting --in=DIR at it. This one has no --in. +nr="$( runAt "$REPO" --no-redact --expand=db_0 2>&1 >/dev/null | head -1 )" +case "$nr" in + *"--in=DIR"*) no "arm 6s2c: a run with NO --in was refused by the --in=DIR branch: $nr" ;; + *) ok "arm 6s2c: --no-redact without --in is untouched by the scoped refusal (stderr: ${nr:-clean})" ;; +esac + +# AND THE MEMBER THE FIRST AUDIT MISSED, plus the derivation that makes a third impossible to miss +# (CodeRabbit, review of #212). The first audit of this class reported "every other flag hits its own pairing +# refusal first, and --external-surface is the only one reaching the generic diagnostic". That predicate was +# measured this time, over the derived universe (test/flaguniverse.py) rather than a sample: 119 of the 171 +# bool/view rows firstFlagOutside walks reach the generic line, and 43 of them answer when run alone. So +# "answers alone" is not the predicate either -- --metrics answers alone, and what it answers IS the default +# map, decorated. The predicate is STRUCTURAL and already in main.cpp: a flag in kMapShapingFlags SHAPES the +# default map instead of answering instead of it, and kMapShapingFlags minus kInRideAlong is exactly +# {--no-redact, --metrics, --map-diff}. --map-diff is the one that genuinely preempts (it takes its own +# ranking branch ahead of churn-decay), so the inert set is the other two. +# +# Arm 6s2e derives that set HERE, from the two tables, and asserts every member says "inert" -- so a flag +# added to kMapShapingFlags tomorrow without a kInRideAlong row is caught tomorrow by this arm rather than by +# the next review. The set is re-read from source on every run; nothing about it is pinned in this file. +refuses "arm 6s2d: --in= with --metrics says INERT, not answers-instead" "inert here" --rank-by=churn-decay --in=db --metrics +refuses "arm 6s2d2 control: --map-diff really does preempt, so it keeps answers-instead" "answers instead" --rank-by=churn-decay --in=db --map-diff + +shapers="$( python3 - "$ROOT/src/main.cpp" <<'PYEOF' +import re, sys +src = open(sys.argv[1]).read() +def table(name): + body = src[src.index("kMapShapingFlags[] =" if name == "shape" else "kInRideAlong[] ="):] + body = body[:body.index("};")] + return set(re.findall(r'"(--[a-z0-9-]+)"', body)) +inert = sorted(table("shape") - table("ride") - {"--map-diff"}) +print(" ".join(inert)) +PYEOF +)" +if [ -z "$shapers" ]; then + no "arm 6s2e: derived the inert-shaper set as EMPTY -- the scrape of kMapShapingFlags/kInRideAlong broke, and an empty set would pass every assertion below vacuously" +else + bad="" + for f in $shapers; do + runAt "$REPO" --rank-by=churn-decay --in=db "$f" >/dev/null 2>"$WORK/shaper.err" /dev/null 2>"$WORK/tk0.err" "$WORK/c.out" 2>"$WORK/c.err" — the typo-reads-as-nothing-changed +# answer the refusal exists to prevent. Each must now refuse, naming the crawl. +ln -s db "$REPO/dblink" 2>/dev/null +refuses "arm 6z1: --in=DIR excluded from the crawl" "no indexed file is under" --rank-by=churn-decay --exclude=db/ --in=db +refuses "arm 6z2: --in=DIR through a symlink alias" "no indexed file is under" --rank-by=churn-decay --in=dblink +if [ -d "$REPO/DB" ]; then # only on a case-folding volume (APFS): elsewhere the fs check refuses first, which is also correct + refuses "arm 6z3: --in=DIR case-folded by the volume" "no indexed file is under" --rank-by=churn-decay --in=DB +else + runAt "$REPO" --rank-by=churn-decay --in=DB >"$WORK/cf.out" 2>"$WORK/cf.err" "$WORK/r.out" 2>"$WORK/r.err" "$WORK/e7" )"; ec=$? +tagS="$( scopedTag "$SP" )" +[ "$ec" = 0 ] && printf '%s' "$tagS" | grep -q '^' | grep -qE 'next=' \ + && printf '%s' '' | grep -qE 'capped=|next=' \ + && ! printf '%s' '' | grep -qE 'next='; then + ok "arm 7c control: the negative pattern sees next= on a mutant tag and stays quiet on a clean one" +else + no "arm 7c control: the negative pattern cannot see next= on a mutant tag — arms 3d and 7c are inert" +fi +DA2="$( run --rank-by=churn-decay "--in=-dash" 2>"$WORK/e7d" )"; ec=$? +[ "$ec" = 0 ] && printf '%s' "$( scopedTag "$DA2" )" | grep -q '^]*>' | head -1 )" | grep -q 'merge_bombs_skipped="0"' \ + && ok "arm 8a: the global block carries merge_bombs_skipped=\"0\"" \ + || no "arm 8a: global block lacks merge_bombs_skipped= ($( printf '%s' "$IN" | grep -oE ']*>' | head -1 ))" +# merge_bombs_skipped= counts the WINDOW's skipped commits, not DIR's. Stamping it on a directory-scoped +# element reads as "N commits under DIR were skipped", which is a wrong answer for any DIR smaller than the +# repository — so it rides the GLOBAL block alone. +printf '%s' "$tag" | grep -q 'merge_bombs_skipped=' \ + && no "arm 8b: the scoped block carries merge_bombs_skipped= — that is the WINDOW's count, not DIR's ($tag)" \ + || ok "arm 8b: the scoped block carries no merge_bombs_skipped= (the window's count rides the global block alone)" + +# ── arm 9: determinism, well-formedness, legends ──────────────────────────────────────────────────── +IN2="$( run --rank-by=churn-decay --in=db 2>/dev/null )" +[ -n "$IN" ] && [ "$IN" = "$IN2" ] \ + && ok "arm 9a: two --in=db runs are byte-identical ($inBytes B)" \ + || no "arm 9a: two --in=db runs differ" +# xmllint's presence is a PREREQUISITE (checked at the top, exit 2): an absent tool cannot make these two +# arms fail, and the `else no "xmllint missing"` that used to sit here reported the environment as a defect. +if printf '%s' "$IN" | xmllint --noout - 2>"$WORK/xl"; then + ok "arm 9b: --in=db output is well-formed XML" +else + no "arm 9b: xmllint rejected --in=db output"; head -3 "$WORK/xl" | sed 's/^/ /' +fi +printf '%s' "$SP" | xmllint --noout - 2>/dev/null \ + && ok "arm 9c: --in='my dir' output is well-formed XML" \ + || no "arm 9c: xmllint rejected --in='my dir' output" +printf '%s' "$IN" | grep -q 'scope=DIR' && printf '%s' "$IN" | grep -q 'symbols stubbed=1 would_show=' \ + && ok "arm 9d: the full legend defines scope= and the stubbed=/would_show= symbols stub" \ + || no "arm 9d: the full legend does not define scope= / the stub" +printf '%s' "$IN" | grep -q 'more than 100 INDEXED files' \ + && ok "arm 9d2: the full legend says merge_bombs_skipped= counts INDEXED files (what the rule measures)" \ + || no "arm 9d2: the full legend still says 'files' where the rule counts INDEXED files" +INC="$( run --rank-by=churn-decay --in=db --legend=compact 2>/dev/null )"; ec=$? +[ "$ec" = 0 ] && printf '%s' "$INC" | grep -q 'scope=' && printf '%s' "$INC" | grep -q 'stubbed=1 would_show=' \ + && ok "arm 9e: the compact legend defines scope= and the stub" \ + || no "arm 9e: compact legend (exit $ec) lacks scope= / the stub: $( printf '%s' "$INC" | grep -oE '' | head -c 400 )" +# ...and DELETES the full clause it restates. Without a kCompactProsePrefixes row the ~640 B in=DIR prose +# survived beside the compact terms and was charged into est_tokens — the defect that row was added for. +printf '%s' "$INC" | grep -q '