From e631b087bfc9a924451b61776f16b40923918fd2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:00:25 -0400 Subject: [PATCH 01/73] perf(strkern): one header for the byte-parallel string kernels, three mirrored paths, gated MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/infra/strkern.h is the single home the owner asked for: NEON, AVX2 and the scalar/SWAR reference written side by side, every kernel taking (const char*, size_t) so nothing needs a NUL terminator. Kernels: classMasks (Lemire two-stage nibble classification -> per-byte upper/lower/digit/alnum bitmasks), lowerFoldAscii / lowerFoldedEquals (Tempesta's (unsigned)(c-'A')<26 fold, with the 0x80-bias spelling for AVX2's signed compare), findByte / find3 / findByteset (StringZilla's (b>>3,b&7) set decomposition and NEON vshrn movemask, Mula's first/last-byte filter). Lane M appends its find kernels here, additively. Two things the audit's technique map got wrong on contact, both recorded in the header: * the familiar has-zero-byte `( x - 0x0101.. ) & ~x & 0x8080..` cannot ship under G1. Its subtraction WRAPS and -fsanitize=integer -fno-sanitize-recover=all turns that into an abort; the harness caught it on the first run. The exact, wrap-free variant `~( ( ( x & 0x7F7F.. ) + 0x7F7F.. ) | x ) & 0x8080..` replaces it and is also strictly better: it has no false positives, so findByte_scalar needs no verify pass at all. * NEON's movemask is used in BOTH forms on purpose. The find kernels take StringZilla's vshrn_n_u16 nibble mask (one shift-and-narrow; ctz>>2 is the byte index), because they only ever want the first match. classMasks takes a true one-bit-per-byte mask, because the tokenizer's boundary algebra needs a shift of one to MEAN one byte, and a nibble form would put a different scale factor in the NEON and AVX2 spellings of every expression that follows. CMake: the x86-64 floor is -march=x86-64-v3 unconditionally for x86-64 targets (owner, 2026-09-10: AVX2 + BMI1/2 + FMA + LZCNT + MOVBE, the RHEL 10 level; never v4). Without it __AVX2__ is undefined and every x86-64 build silently runs the scalar twins. The FMA caveat and its remedy (-ffp-contract=off on the pagerank TU, never lowering the floor) are written into the branch. Apple Silicon and aarch64 Linux are untouched — NEON is baseline there. GATE (written before the wiring it will measure): test/strkerncheck.sh + strkern_harness.cpp. Four arms, all green on this box: PASS 13 harness arms green (path=NEON block=16) PASS non-vacuity: strkern path: NEON on arm64 PASS can-go-red: -DSTRKERN_MUTATE=1 fails 8 arm(s) as designed PASS x86_64/AVX2 mirror runs green under Rosetta 2 (13 arms) Corpora: 100k fixed-seed random buffers over four alphabets (identifier, full ASCII, high-bit, camel/acronym-dense), lengths 0..300 so every 16- and 32-byte boundary is straddled repeatedly; all 256 byte values at every offset and length; and every byte of src/ and docs/ (751 files). The x86_64 arm compiles the same harness -arch x86_64 -march=x86-64-v3 and runs it under Rosetta 2, so the AVX2 mirror is proven HERE and not only on CI's ubuntu legs. -DSTRKERN_MUTATE=1 perturbs the SIMD tables only (one nibble-table bit, the fold range by one, findByteset's high half) and must fail: it fails 8 arms. test/portablebuildcheck.sh gains arms #2b/#2c: an x86-64 target must carry the v3 floor and neither v4 nor an Apple flag; an aarch64 target must not be handed an x86 -march. Both drive the REAL module via a CMAKE_SYSTEM_PROCESSOR override, not a reimplementation. Registered in test/regression.sh, exempted in binoverridecheck (it builds its own harnesses), gate count regenerated by docs/gatecount_build.py: 586 -> 587. --- README.md | 4 +- cmake/PortableFlags.cmake | 39 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/infra/strkern.h | 677 ++++++++++++++++++++++++++++++++ test/binoverridecheck.sh | 1 + test/portablebuildcheck.sh | 44 ++- test/regression.sh | 2 +- test/strkern_harness.cpp | 690 +++++++++++++++++++++++++++++++++ test/strkerncheck.sh | 144 +++++++ 10 files changed, 1595 insertions(+), 18 deletions(-) create mode 100644 src/infra/strkern.h create mode 100644 test/strkern_harness.cpp create mode 100755 test/strkerncheck.sh diff --git a/README.md b/README.md index dde8da76a..d157ea9f3 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-586 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +587 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 **586 gate scripts** and is the authoritative list; +`test/regression.sh` names **587 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/cmake/PortableFlags.cmake b/cmake/PortableFlags.cmake index 60f328439..c69d6451d 100644 --- a/cmake/PortableFlags.cmake +++ b/cmake/PortableFlags.cmake @@ -9,7 +9,11 @@ # -DRIPWIRE_NATIVE=ON dev-machine-only opt-in: -march=native bakes in whatever ISA extensions the # CONFIGURING host happens to have. Never use for a binary that will run on any # other machine (a release artifact, CI, a teammate's laptop). -# default (OFF) portable. On real Apple Silicon, -mcpu=apple-m1 is safe GENERIC tuning (every +# default (OFF) portable, WITH ONE ARCHITECTURE FLOOR: an x86-64 target gets -march=x86-64-v3 +# (owner, 2026-09-10 — see the branch below for why, and for the FMA caveat). +# That is an architecture LEVEL, not a host bake-in, and it is what makes +# src/infra/strkern.h's AVX2 kernels compile at all. +# On real Apple Silicon, -mcpu=apple-m1 is safe GENERIC tuning (every # shipping Apple Silicon core, M1 through the current line, is an M1-superset — # this is not a native-host bake-in), so we auto-apply it there. The moment we are # NOT on Apple Silicon — any Linux, x86-64 macOS, or cross build — we must emit @@ -34,13 +38,42 @@ if(APPLE AND NOT RIPWIRE_PRETEND_LINUX AND CMAKE_SYSTEM_PROCESSOR MATCHES "^(arm set(RIPWIRE_IS_APPLE_SILICON ON) endif() +set(RIPWIRE_IS_X86_64 OFF) +if(CMAKE_SYSTEM_PROCESSOR MATCHES "^(x86_64|amd64|AMD64)$") + set(RIPWIRE_IS_X86_64 ON) +endif() + if(RIPWIRE_NATIVE) set(RIPWIRE_ARCH_FLAGS -O3 -march=native -ffast-math -fno-finite-math-only) elseif(RIPWIRE_IS_APPLE_SILICON) set(RIPWIRE_ARCH_FLAGS -O2 -mcpu=apple-m1 -ffast-math -fno-finite-math-only) +elseif(RIPWIRE_IS_X86_64) + # ── the x86-64 FLOOR (owner decision, 2026-09-10) ──────────────────────────────────────────────── + # "No machine older than ten years", and then "is v3 better? do not want to go backwards". So the + # shipped x86-64 binary requires x86-64-v3: AVX2 + BMI1/BMI2 + FMA + LZCNT + MOVBE — the same floor + # RHEL 10 sets, hardware from 2015 (Haswell/Excavator) onward. This is NOT a host bake-in: the flag + # names an ARCHITECTURE LEVEL that every supported x86-64 target implements, exactly as -mcpu=apple-m1 + # names a generic Apple Silicon level above. It is unconditional rather than a runtime dispatch + # because a dispatch table is a second code path nothing here would keep honest (G3: one deterministic + # build step, no host-installed anything). + # + # WHAT DEPENDS ON IT: src/infra/strkern.h compiles its AVX2 mirror behind `__AVX2__`, which v3 defines. + # Without this line an x86-64 build silently drops to the scalar twins — correct, and several times + # slower on every text-scanning verb. arm64 needs no counterpart: NEON is in the arm64 baseline. + # + # NEVER v4 (AVX-512): the downclocking and the fragmented server support make it a portability loss, + # and nothing here is 512-bit-shaped. + # + # THE FMA CAVEAT, spelled out because it is the one way this line could move a NUMBER rather than a + # timing: v3 includes FMA, and a compiler is allowed to contract `a*b+c` into one fused instruction + # with a single rounding, which changes float results. src/pagerank.cpp — the only translation unit + # whose float reassociation the determinism contract pins (docs/ARCHITECTURE.md §3) — is already + # compiled with -fno-fast-math by CMakeLists.txt. If a pinned float gate ever moves on the Ubuntu CI + # legs, the fix is `-ffp-contract=off` on THAT translation unit, never lowering this floor. + set(RIPWIRE_ARCH_FLAGS -O2 -march=x86-64-v3 -ffast-math -fno-finite-math-only) else() - # Portable default: no host- or vendor-specific ISA flag at all. Compiles clean on any x86-64/aarch64 - # target (Linux, Intel macOS, BSD, a cross toolchain) with generic -O2 codegen. + # Portable default: no host- or vendor-specific ISA flag at all. Compiles clean on any non-x86-64, + # non-Apple-Silicon target (aarch64 Linux, BSD, a cross toolchain) with generic -O2 codegen. set(RIPWIRE_ARCH_FLAGS -O2 -ffast-math -fno-finite-math-only) endif() diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..1b1058118 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` | 586 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 587 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) @@ -5625,7 +5625,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 **586 gate scripts**, all of which exist on disk. +naming **587 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`. @@ -6637,7 +6637,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 586. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 587. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 0647c42fe..fc5a0220f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["586 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 + ["587 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "586", "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, "587", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["586 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/infra/strkern.h b/src/infra/strkern.h new file mode 100644 index 000000000..e3ade8539 --- /dev/null +++ b/src/infra/strkern.h @@ -0,0 +1,677 @@ +#pragma once + +// strkern.h — THE byte-parallel string kernels. One header, three mirrored paths, no other home. +// +// House rule (owner, 2026-09-10): every SIMD string kernel ripwire owns lives HERE, as inline functions +// with the NEON, the AVX2 and the scalar/SWAR reference written side by side in one place, so a reader +// can diff the three by eye and a reviewer can see immediately when one path drifted. No SIMD intrinsic +// for string work exists outside this header. (The pre-existing vector code in fixedStr.h, radixSort.h, +// sparseCsr.h and dynamic_map.hpp is NOT string work and stays where it is.) +// +// Every kernel takes ( const char*, std::size_t ) — never a NUL terminator, never a std::string — so a +// future SIMD-backed string type can adopt them unchanged, and so a kernel can run over the interior of a +// mapped file. Every kernel has a `_scalar` twin that is ALWAYS compiled and always callable: it is the +// oracle test/strkerncheck.sh compares the vector path against, and it is the code that actually runs on +// a target with neither NEON nor AVX2. The scalar twins are portable C++ — no intrinsic, no UB, no +// unaligned type-punned load (every wide read goes through std::memcpy). +// +// ── ATTRIBUTION ────────────────────────────────────────────────────────────────────────────────────── +// * Daniel Lemire, 2023-07-13 ("Fast Unicode/ASCII character classification", sse_type.c / upper_type.c): +// the two-stage nibble-table classification this file's classMasks is built on — the low nibble indexes +// a 16-byte column bitmap, the high nibble selects a row bit, AND them, a nonzero result is a member +// and its bit says WHICH class. Also the SWAR case-fold identity used by the fused subtoken hash. +// * Tempesta Technologies (`match_symbols_mask32_c`, and the strcasecmp kernel): the branchless A-Z fold +// as `(unsigned)( c - 'A' ) < 26` — one wrapping subtract and one unsigned compare — and its SSE +// 0x80-bias spelling for a signed-only compare instruction. +// * StringZilla (`find/neon.h`, `find/serial.h`, Ash Vardanian): the NEON movemask via +// `vshrn_n_u16( ..., 4 )` + `vget_lane_u64` (four bits per byte, one shift-and-narrow instead of the +// bitmask-and-horizontal-add x86 gets for free from `pmovmskb`); `sz_find_byteset`'s 256-bit set test +// as two table lookups over the (byte>>3, byte&7) decomposition; the SWAR has-zero-byte probe. +// * Wojciech Muła: the `pshufb` byte-classification family these two-stage lookups descend from, and +// the "check an anomalous first and last byte, then verify" shape find3 uses. +// +// ── THE F3 LESSON (memory `optremarks-f3-strcmp`, PR #107) ─────────────────────────────────────────── +// A compare that DECIDES at byte 0 or 1 is call overhead, not string work: vectorizing it loses, and F3's +// measured win came from deleting the call, not from widening it. SIMD pays only where a loop must touch +// EVERY byte of a text. That is why classMasks (which reads every byte of every doc/body field) is wired +// into the tokenizer, while lowerFoldedEquals below — the acronym-seam fallback, reached only after a +// length-and-head filter has already rejected almost everything, on tokens averaging ~7 bytes — ships as +// a kernel with a parity arm but is deliberately NOT wired into lexical.h's scan loop. Measure before +// widening a short compare. +// +// ── PORTABILITY ────────────────────────────────────────────────────────────────────────────────────── +// arm64 has NEON in the baseline (`__ARM_NEON` is defined by every arm64 toolchain). x86-64 builds carry +// `-march=x86-64-v3` unconditionally (CMakeLists.txt; owner decision 2026-09-10 — AVX2 + BMI1/2 + FMA + +// LZCNT + MOVBE, the RHEL 10 floor), so `__AVX2__` is defined on every shipped x86-64 binary. Anything +// else — a cross build, a hand-configured toolchain that overrides the arch flags — compiles the scalar +// twins, which are the same functions with the same contracts and no ISA requirement at all. +// +// Determinism (docs/ARCHITECTURE.md §3): every kernel here is INTEGER and EXACT. Its result is a bit +// pattern, not a rounded sum, so no path can reassociate its way to a different answer; the NEON, AVX2 +// and scalar paths return identical values for identical input, which is precisely what +// test/strkerncheck.sh asserts on 100k random buffers and on every byte of src/ and docs/. + +#include +#include +#include +#include + +#if defined( __ARM_NEON ) + #include +#elif defined( __AVX2__ ) + #include +#endif + +namespace rw::strkern +{ + +// ── the compiled path, named so a harness banner can prove it is not vacuously testing scalar-vs-scalar ── +#if defined( __ARM_NEON ) +inline constexpr const char* kPathName = "NEON"; +inline constexpr std::size_t kBlockBytes = 16; // one uint8x16_t +#elif defined( __AVX2__ ) +inline constexpr const char* kPathName = "AVX2"; +inline constexpr std::size_t kBlockBytes = 32; // one __m256i +#else +inline constexpr const char* kPathName = "scalar"; +inline constexpr std::size_t kBlockBytes = 16; // the scalar twins accept any n <= 32; 16 keeps the + // block loop's shape identical on every path +#endif + +// The largest block any path uses. A caller that wants ONE stack buffer big enough for every path +// (the harness does) sizes it by this, not by kBlockBytes. +inline constexpr std::size_t kMaxBlockBytes = 32; + +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// 1. classMasks — per-byte character-class bitmasks over one block +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// +// One BIT per byte, bit k = byte k of the block (LSB first). Bits at or above `n` are always 0, so a +// short tail needs no separate mask from the caller. `alnum` is exactly `upper | lower | digit`; a +// SEPARATOR is a byte with no class, i.e. a clear bit in `alnum` below n — the tokenizer never needs a +// separate separator mask, it needs `~alnum & validBits`, which only the caller knows the width of. +struct Masks +{ + std::uint32_t alnum = 0; // [A-Za-z0-9] + std::uint32_t upper = 0; // [A-Z] + std::uint32_t lower = 0; // [a-z] + std::uint32_t digit = 0; // [0-9] +}; + +// ── the two-stage nibble table (Lemire 2023/07/13) ────────────────────────────────────────────────── +// A class is a set of bytes, and pshufb/vqtbl1q can only look up 16 entries — so a byte's membership is +// decomposed into "what its LOW nibble allows" AND "what its HIGH nibble allows", one bit per (class, +// high-nibble) pair. Five bits are enough because the three classes span five (high-nibble, low-range) +// rectangles: +// +// bit 0 0x01 upper-A high nibble 4, low nibble 1..F 'A'(0x41) .. 'O'(0x4F) +// bit 1 0x02 upper-B high nibble 5, low nibble 0..A 'P'(0x50) .. 'Z'(0x5A) +// bit 2 0x04 lower-A high nibble 6, low nibble 1..F 'a'(0x61) .. 'o'(0x6F) +// bit 3 0x08 lower-B high nibble 7, low nibble 0..A 'p'(0x70) .. 'z'(0x7A) +// bit 4 0x10 digit high nibble 3, low nibble 0..9 '0'(0x30) .. '9'(0x39) +// +// The high-nibble table has AT MOST ONE bit per entry, so `lowTable[lo] & highTable[hi]` has at most one +// bit set and never confuses two classes. A byte >= 0x80 has a high nibble of 8..F, every one of which +// maps to 0 — so the whole non-ASCII half is a separator by construction, with no extra compare. A zero +// byte maps to 0 as well, which is what makes zero-padding a short tail safe. +inline constexpr std::uint8_t kClsUpper = 0x03; // upper-A | upper-B +inline constexpr std::uint8_t kClsLower = 0x0C; // lower-A | lower-B +inline constexpr std::uint8_t kClsDigit = 0x10; + +// STRKERN_MUTATE is the gate's can-go-red arm: it flips ONE bit of the SIMD-only nibble table, which the +// scalar oracle does not consult. A build with it defined must make test/strkerncheck.sh fail; if it does +// not, the parity assertion is vacuous and the gate is worthless. Never define it in a real build. +inline constexpr std::uint8_t kLowNibbleTable[ 16 ] = { + /* 0 */ 0x1A, // lo 0: upper-B | lower-B | digit + /* 1 */ 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, 0x1F, // lo 1..9: every rectangle + /* A */ 0x0F, // lo A: upper-A | upper-B | lower-A | lower-B (no digit — ':' is not one) + /* B */ 0x05, 0x05, 0x05, 0x05, 0x05 // lo B..F: only the ..A rectangles +}; +inline constexpr std::uint8_t kHighNibbleTable[ 16 ] = { + 0x00, 0x00, 0x00, + /* 3 */ 0x10, // '0'..'?' +#if defined( STRKERN_MUTATE ) + /* 4 */ 0x03, // MUTATION: upper-A's row bit widened — 'P'..'Z' now also + // report upper-A, so '@'(0x40) misclassifies. SIMD only. +#else + /* 4 */ 0x01, // '@'..'O' +#endif + /* 5 */ 0x02, // 'P'..'_' + /* 6 */ 0x04, // '`'..'o' + /* 7 */ 0x08, // 'p'..DEL + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 +}; + +// ── the scalar oracle: the class definitions, restated with no table and no intrinsic ──────────────── +// Deliberately written as the three range tests a reader would write by hand, NOT as a lookup — an oracle +// that shared the tables would move with them and prove nothing. Accepts any n <= kMaxBlockBytes, so it +// can validate a 32-byte AVX2 block and a 16-byte NEON block from the same harness arm. +inline void classMasks_scalar( const char* p, std::size_t n, Masks& out ) noexcept +{ + out = Masks{}; + if( n > kMaxBlockBytes ) + { + n = kMaxBlockBytes; + } + for( std::size_t k = 0; k < n; ++k ) + { + const unsigned char c = static_cast( p[ k ] ); + const std::uint32_t bit = std::uint32_t( 1 ) << k; + if( c >= 'A' && c <= 'Z' ) + { + out.upper |= bit; + } + else if( c >= 'a' && c <= 'z' ) + { + out.lower |= bit; + } + else if( c >= '0' && c <= '9' ) + { + out.digit |= bit; + } + } + out.alnum = out.upper | out.lower | out.digit; +} + +#if defined( __ARM_NEON ) +// StringZilla's NEON movemask: `vshrn_n_u16( x, 4 )` narrows sixteen u16 lanes to eight u8 lanes taking +// four bits from each, so a byte-wide all-ones/all-zeros compare result becomes a u64 with a NIBBLE per +// input byte. One shift-and-narrow plus one lane read; `countr_zero( m ) >> 2` is the first match's byte +// index. Used by the find kernels, which only ever want that index. +inline std::uint64_t neonNibbleMask( uint8x16_t cmp ) noexcept +{ + return vget_lane_u64( vreinterpret_u64_u8( vshrn_n_u16( vreinterpretq_u16_u8( cmp ), 4 ) ), 0 ); +} + +// One BIT per byte (16 bits), which is what the tokenizer's mask ALGEBRA needs — there a shift by one +// must mean "one byte over", and the nibble form's shift by four would work but would make every mask +// expression carry a scale factor that differs between NEON and AVX2. Four instructions: AND with the +// per-lane bit weights, then two horizontal byte sums. +inline std::uint32_t neonByteMask( uint8x16_t cmp ) noexcept +{ + static constexpr std::uint8_t kBitWeights[ 16 ] = { 1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128 }; + const uint8x16_t m = vandq_u8( cmp, vld1q_u8( kBitWeights ) ); + return std::uint32_t( vaddv_u8( vget_low_u8( m ) ) ) | ( std::uint32_t( vaddv_u8( vget_high_u8( m ) ) ) << 8 ); +} +#endif + +// Per-byte class masks for ONE block. `n` must be <= kBlockBytes; bits at or above n are 0. Never reads +// past p + n: a short block is copied into a zero-filled stack buffer first (a zero byte classifies as a +// separator, so the padding cannot invent a class bit). +inline void classMasks( const char* p, std::size_t n, Masks& out ) noexcept +{ +#if defined( __ARM_NEON ) || defined( __AVX2__ ) + alignas( 32 ) std::uint8_t padded[ kBlockBytes ] = {}; + if( n < kBlockBytes ) + { + std::memcpy( padded, p, n ); + p = reinterpret_cast( padded ); + } +#endif + +#if defined( __ARM_NEON ) + const uint8x16_t v = vld1q_u8( reinterpret_cast( p ) ); + const uint8x16_t lo = vandq_u8( v, vdupq_n_u8( 0x0F ) ); + const uint8x16_t hi = vshrq_n_u8( v, 4 ); // u8 shift is logical: 0..15 always + const uint8x16_t rowLo = vqtbl1q_u8( vld1q_u8( kLowNibbleTable ), lo ); + const uint8x16_t rowHi = vqtbl1q_u8( vld1q_u8( kHighNibbleTable ), hi ); + const uint8x16_t cls = vandq_u8( rowLo, rowHi ); + out.upper = neonByteMask( vtstq_u8( cls, vdupq_n_u8( kClsUpper ) ) ); + out.lower = neonByteMask( vtstq_u8( cls, vdupq_n_u8( kClsLower ) ) ); + out.digit = neonByteMask( vtstq_u8( cls, vdupq_n_u8( kClsDigit ) ) ); + out.alnum = out.upper | out.lower | out.digit; +#elif defined( __AVX2__ ) + // `_mm256_shuffle_epi8` shuffles WITHIN each 128-bit lane, so both nibble tables are broadcast to + // both lanes — that is the whole difference from the NEON spelling. It also differs from vqtbl1q in + // out-of-range behaviour: vqtbl1q returns 0 for an index >= 16 while pshufb takes index & 15 unless + // bit 7 is set, so both indices are masked to 0..15 BEFORE the lookup rather than relying on either. + const __m256i v = _mm256_loadu_si256( reinterpret_cast( p ) ); + const __m256i loTab = _mm256_broadcastsi128_si256( _mm_loadu_si128( reinterpret_cast( kLowNibbleTable ) ) ); + const __m256i hiTab = _mm256_broadcastsi128_si256( _mm_loadu_si128( reinterpret_cast( kHighNibbleTable ) ) ); + const __m256i nibbleMask = _mm256_set1_epi8( 0x0F ); + const __m256i lo = _mm256_and_si256( v, nibbleMask ); + const __m256i hi = _mm256_and_si256( _mm256_srli_epi16( v, 4 ), nibbleMask ); // no epi8 shift exists + const __m256i cls = _mm256_and_si256( _mm256_shuffle_epi8( loTab, lo ), _mm256_shuffle_epi8( hiTab, hi ) ); + const __m256i zero = _mm256_setzero_si256(); + const auto nonzeroMask = [ & ]( std::uint8_t bits ) noexcept + { + // cmpeq-with-zero marks the bytes that are NOT members; invert to get the membership mask. Every + // one of the 32 bits is meaningful (padding bytes are zero, hence non-members), so a plain + // bitwise NOT is correct with no width bookkeeping. + const __m256i isZero = _mm256_cmpeq_epi8( _mm256_and_si256( cls, _mm256_set1_epi8( char( bits ) ) ), zero ); + return ~std::uint32_t( _mm256_movemask_epi8( isZero ) ); + }; + out.upper = nonzeroMask( kClsUpper ); + out.lower = nonzeroMask( kClsLower ); + out.digit = nonzeroMask( kClsDigit ); + out.alnum = out.upper | out.lower | out.digit; +#else + classMasks_scalar( p, n, out ); +#endif +} + +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// 2. lowerFoldAscii / lowerFoldedEquals — the A-Z-only fold +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// +// A-Z ONLY, on purpose: this is the fold ripwire's lexical layer means by "lowercase" (lexindex.h +// lexLowerByte), and it is the only one that is locale-free, byte-exact and reversible enough to hash +// with. Bytes >= 0x80 are left alone — a UTF-8 continuation byte is not a letter to fold. +// +// The identity (Tempesta): `(unsigned char)( c - 'A' ) < 26` is true exactly for 'A'..'Z', because the +// subtraction wraps, so every byte below 'A' lands at 230..255 and every byte above 'Z' at 26..229. NEON +// has an unsigned compare (`vcltq_u8`) and spells it directly. AVX2's byte compare is SIGNED only, so the +// same test is biased by 0x80: subtracting `'A' + 0x80` produces `( c - 'A' ) ^ 0x80`, and the unsigned +// `< 26` becomes the signed `> -103` with the constant on the left. Same predicate, one instruction each. + +// SWAR fold of eight bytes at once (Lemire's formulation, high-bit arithmetic only, no per-byte branch). +// `heptets` clears bit 7 so a byte >= 0x80 cannot borrow into its neighbour's compare; the final `& ~x` +// then excludes those bytes from the fold, since their true value was never in 'A'..'Z'. +inline std::uint64_t swarLowerFold8( std::uint64_t x ) noexcept +{ + constexpr std::uint64_t kOnes = 0x0101010101010101ull; + constexpr std::uint64_t kHighs = 0x8080808080808080ull; + const std::uint64_t heptets = x & ( 0x7Full * kOnes ); + const std::uint64_t geA = ( heptets + ( 0x80ull - 'A' ) * kOnes ) & kHighs; // byte >= 'A' + const std::uint64_t gtZ = ( heptets + ( 0x7Full - 'Z' ) * kOnes ) & kHighs; // byte > 'Z' + const std::uint64_t isUpper = geA & ~gtZ & ~x; // and ASCII + return x | ( isUpper >> 2 ); // 0x80 >> 2 == 0x20 +} + +inline void lowerFoldAscii_scalar( char* p, std::size_t n ) noexcept +{ + std::size_t k = 0; + for( ; k + 8 <= n; k += 8 ) + { + std::uint64_t x = 0; + std::memcpy( &x, p + k, 8 ); + x = swarLowerFold8( x ); + std::memcpy( p + k, &x, 8 ); + } + for( ; k < n; ++k ) + { + const unsigned char c = static_cast( p[ k ] ); + if( c >= 'A' && c <= 'Z' ) + { + p[ k ] = char( c + 0x20 ); + } + } +} + +inline void lowerFoldAscii( char* p, std::size_t n ) noexcept +{ + std::size_t k = 0; +#if defined( __ARM_NEON ) + #if defined( STRKERN_MUTATE ) + const uint8x16_t kSpan = vdupq_n_u8( 25 ); // MUTATION: 'Z' stops folding. SIMD only. + #else + const uint8x16_t kSpan = vdupq_n_u8( 26 ); + #endif + for( ; k + 16 <= n; k += 16 ) + { + auto* q = reinterpret_cast( p + k ); + const uint8x16_t v = vld1q_u8( q ); + const uint8x16_t isUp = vcltq_u8( vsubq_u8( v, vdupq_n_u8( 'A' ) ), kSpan ); + vst1q_u8( q, vorrq_u8( v, vandq_u8( isUp, vdupq_n_u8( 0x20 ) ) ) ); + } +#elif defined( __AVX2__ ) + #if defined( STRKERN_MUTATE ) + const __m256i kBound = _mm256_set1_epi8( char( 0x99 ) ); // MUTATION: 'Z' stops folding. SIMD only. + #else + const __m256i kBound = _mm256_set1_epi8( char( 0x9A ) ); // 26 ^ 0x80, i.e. -102 signed + #endif + for( ; k + 32 <= n; k += 32 ) + { + auto* q = reinterpret_cast<__m256i*>( p + k ); + const __m256i v = _mm256_loadu_si256( q ); + const __m256i biased = _mm256_sub_epi8( v, _mm256_set1_epi8( char( 'A' + 0x80 ) ) ); + const __m256i isUp = _mm256_cmpgt_epi8( kBound, biased ); + _mm256_storeu_si256( q, _mm256_or_si256( v, _mm256_and_si256( isUp, _mm256_set1_epi8( 0x20 ) ) ) ); + } +#endif + lowerFoldAscii_scalar( p + k, n - k ); +} + +// Does `a[0..n)` equal `bLowered[0..n)` once a is A-Z-folded? `bLowered` must ALREADY be all-lowercase — +// the caller's query token is, by construction. See the F3 note at the top of this header before wiring +// this into a hot path: it is a late-deciding compare over short spans and the scalar form usually wins. +inline bool lowerFoldedEquals_scalar( const char* a, const char* bLowered, std::size_t n ) noexcept +{ + std::size_t k = 0; + for( ; k + 8 <= n; k += 8 ) + { + std::uint64_t x = 0, y = 0; + std::memcpy( &x, a + k, 8 ); + std::memcpy( &y, bLowered + k, 8 ); + if( swarLowerFold8( x ) != y ) + { + return false; + } + } + for( ; k < n; ++k ) + { + const unsigned char c = static_cast( a[ k ] ); + const unsigned char f = ( c >= 'A' && c <= 'Z' ) ? static_cast( c + 0x20 ) : c; + if( f != static_cast( bLowered[ k ] ) ) + { + return false; + } + } + return true; +} + +inline bool lowerFoldedEquals( const char* a, const char* bLowered, std::size_t n ) noexcept +{ + std::size_t k = 0; +#if defined( __ARM_NEON ) + for( ; k + 16 <= n; k += 16 ) + { + const uint8x16_t v = vld1q_u8( reinterpret_cast( a + k ) ); + const uint8x16_t isUp = vcltq_u8( vsubq_u8( v, vdupq_n_u8( 'A' ) ), vdupq_n_u8( 26 ) ); + const uint8x16_t fold = vorrq_u8( v, vandq_u8( isUp, vdupq_n_u8( 0x20 ) ) ); + const uint8x16_t other = vld1q_u8( reinterpret_cast( bLowered + k ) ); + if( vminvq_u8( vceqq_u8( fold, other ) ) != 0xFF ) + { + return false; + } + } +#elif defined( __AVX2__ ) + for( ; k + 32 <= n; k += 32 ) + { + const __m256i v = _mm256_loadu_si256( reinterpret_cast( a + k ) ); + const __m256i biased = _mm256_sub_epi8( v, _mm256_set1_epi8( char( 'A' + 0x80 ) ) ); + const __m256i isUp = _mm256_cmpgt_epi8( _mm256_set1_epi8( char( 0x9A ) ), biased ); + const __m256i fold = _mm256_or_si256( v, _mm256_and_si256( isUp, _mm256_set1_epi8( 0x20 ) ) ); + const __m256i other = _mm256_loadu_si256( reinterpret_cast( bLowered + k ) ); + if( std::uint32_t( _mm256_movemask_epi8( _mm256_cmpeq_epi8( fold, other ) ) ) != 0xFFFFFFFFu ) + { + return false; + } + } +#endif + return lowerFoldedEquals_scalar( a + k, bLowered + k, n - k ); +} + +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// 3. findByte / find3 / findByteset — first-occurrence scans +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// +// All three return the index of the first match, or `n` when there is none — never a sentinel that a +// caller could confuse with a valid index, and never std::string_view::npos (these kernels do not know +// what a string_view is). + +// SWAR has-zero-byte (StringZilla serial; the exact variant, not the borrow one). Sets 0x80 in EVERY zero +// byte of x and in no other byte — the result is exact, so a caller can act on ctz( m ) >> 3 without a +// verify pass. +// +// TWO REASONS THIS IS NOT THE FAMILIAR `( x - 0x0101… ) & ~x & 0x8080…`. First, that form is only +// approximate: a borrow running out of a zero byte can set the high bit of the NEXT byte too (0x0001 +// reports both bytes), so every candidate needs re-checking. Second, and decisive here, its subtraction +// WRAPS, and G1 compiles with `-fsanitize=integer -fno-sanitize-recover=all`: an unsigned wrap is an +// immediate abort, which is exactly how this landed red the first time. The form below never wraps at all +// — per byte, `( v & 0x7F ) + 0x7F <= 0xFE`, so no carry crosses a byte boundary or leaves bit 63. +// +// Why it is exact: `( v & 0x7F ) + 0x7F` has its high bit set iff the low seven bits of v are nonzero, and +// OR-ing v back in adds the case "v's own high bit is set". So the high bit of `t | v` is CLEAR exactly +// when v == 0; invert and mask. +inline constexpr std::uint64_t swarZeroByteMask( std::uint64_t x ) noexcept +{ + constexpr std::uint64_t kLows = 0x7F7F7F7F7F7F7F7Full; + constexpr std::uint64_t kHighs = 0x8080808080808080ull; + return ~( ( ( x & kLows ) + kLows ) | x ) & kHighs; +} + +inline std::size_t findByte_scalar( const char* p, std::size_t n, char needle ) noexcept +{ + constexpr std::uint64_t kOnes = 0x0101010101010101ull; + const std::uint64_t splat = kOnes * static_cast( needle ); + std::size_t k = 0; + for( ; k + 8 <= n; k += 8 ) + { + std::uint64_t x = 0; + std::memcpy( &x, p + k, 8 ); + const std::uint64_t m = swarZeroByteMask( x ^ splat ); + if( m != 0 ) + { + return k + ( std::size_t( __builtin_ctzll( m ) ) >> 3 ); // the mask is EXACT: no verify pass + } + } + for( ; k < n; ++k ) + { + if( p[ k ] == needle ) + { + return k; + } + } + return n; +} + +inline std::size_t findByte( const char* p, std::size_t n, char needle ) noexcept +{ + std::size_t k = 0; +#if defined( __ARM_NEON ) + const uint8x16_t splat = vdupq_n_u8( static_cast( needle ) ); + for( ; k + 16 <= n; k += 16 ) + { + const uint8x16_t v = vld1q_u8( reinterpret_cast( p + k ) ); + const std::uint64_t m = neonNibbleMask( vceqq_u8( v, splat ) ); + if( m != 0 ) + { + return k + ( std::size_t( __builtin_ctzll( m ) ) >> 2 ); // four mask bits per input byte + } + } +#elif defined( __AVX2__ ) + const __m256i splat = _mm256_set1_epi8( needle ); + for( ; k + 32 <= n; k += 32 ) + { + const __m256i v = _mm256_loadu_si256( reinterpret_cast( p + k ) ); + const std::uint32_t m = std::uint32_t( _mm256_movemask_epi8( _mm256_cmpeq_epi8( v, splat ) ) ); + if( m != 0 ) + { + return k + std::size_t( __builtin_ctz( m ) ); + } + } +#endif + const std::size_t tail = findByte_scalar( p + k, n - k, needle ); + return tail == n - k ? n : k + tail; +} + +// Three-byte needle — the trigram probe shape. Muła's "check an anomalous first and last byte, then +// verify": the first and THIRD bytes are compared in parallel across a whole block, and only the handful +// of positions where both agree pay a three-byte verify. Returns n when the needle does not occur, and +// when n < 3. +inline std::size_t find3_scalar( const char* p, std::size_t n, const char* needle ) noexcept +{ + if( n < 3 ) + { + return n; + } + constexpr std::uint64_t kOnes = 0x0101010101010101ull; + const std::uint64_t n0 = kOnes * static_cast( needle[ 0 ] ); + const std::uint64_t n2 = kOnes * static_cast( needle[ 2 ] ); + std::size_t k = 0; + while( k + 10 <= n ) // needs 8 bytes at k AND 8 bytes at k + 2 + { + std::uint64_t a = 0, b = 0; + std::memcpy( &a, p + k, 8 ); + std::memcpy( &b, p + k + 2, 8 ); + // both masks are exact, so their AND marks exactly the positions where bytes 0 and 2 of the + // needle agree; only byte 1 is left to verify (Mula's first/last-byte filter, serial spelling). + std::uint64_t m = swarZeroByteMask( a ^ n0 ) & swarZeroByteMask( b ^ n2 ); + while( m != 0 ) + { + const std::size_t at = k + ( std::size_t( __builtin_ctzll( m ) ) >> 3 ); + if( p[ at + 1 ] == needle[ 1 ] ) + { + return at; + } + m &= m - 1; + } + k += 8; + } + for( ; k + 3 <= n; ++k ) + { + if( std::memcmp( p + k, needle, 3 ) == 0 ) + { + return k; + } + } + return n; +} + +inline std::size_t find3( const char* p, std::size_t n, const char* needle ) noexcept +{ + if( n < 3 ) + { + return n; + } + std::size_t k = 0; +#if defined( __ARM_NEON ) + const uint8x16_t s0 = vdupq_n_u8( static_cast( needle[ 0 ] ) ); + const uint8x16_t s2 = vdupq_n_u8( static_cast( needle[ 2 ] ) ); + while( k + 18 <= n ) // 16 bytes at k AND 16 bytes at k + 2 + { + const uint8x16_t v0 = vld1q_u8( reinterpret_cast( p + k ) ); + const uint8x16_t v2 = vld1q_u8( reinterpret_cast( p + k + 2 ) ); + std::uint64_t m = neonNibbleMask( vandq_u8( vceqq_u8( v0, s0 ), vceqq_u8( v2, s2 ) ) ); + while( m != 0 ) + { + // one NIBBLE per input byte, so the lowest set bit sits at 4 * byteIndex and clearing the + // candidate means clearing its whole nibble — `m &= m - 1` (the bit-per-byte idiom) would + // spin on the other three bits of the same byte. + const int lowBit = __builtin_ctzll( m ); + const std::size_t at = k + ( std::size_t( lowBit ) >> 2 ); + if( p[ at + 1 ] == needle[ 1 ] ) + { + return at; + } + m &= ~( 0xFull << ( lowBit & ~3 ) ); + } + k += 16; + } +#elif defined( __AVX2__ ) + const __m256i s0 = _mm256_set1_epi8( needle[ 0 ] ); + const __m256i s2 = _mm256_set1_epi8( needle[ 2 ] ); + while( k + 34 <= n ) // 32 bytes at k AND 32 bytes at k + 2 + { + const __m256i v0 = _mm256_loadu_si256( reinterpret_cast( p + k ) ); + const __m256i v2 = _mm256_loadu_si256( reinterpret_cast( p + k + 2 ) ); + std::uint32_t m = std::uint32_t( _mm256_movemask_epi8( + _mm256_and_si256( _mm256_cmpeq_epi8( v0, s0 ), _mm256_cmpeq_epi8( v2, s2 ) ) ) ); + while( m != 0 ) + { + const std::size_t at = k + std::size_t( __builtin_ctz( m ) ); + if( p[ at + 1 ] == needle[ 1 ] ) + { + return at; + } + m &= m - 1; + } + k += 32; + } +#endif + const std::size_t tail = find3_scalar( p + k, n - k, needle ); + return tail == n - k ? n : k + tail; +} + +// A 256-bit byte set, laid out the way `sz_find_byteset` wants it: bit ( b & 7 ) of byte ( b >> 3 ). That +// decomposition is what makes the SIMD test two table lookups — the (b >> 3) lookup fetches the set's row +// byte, the (b & 7) lookup fetches the bit to test it with. +struct Byteset256 +{ + std::uint8_t bits[ 32 ] = {}; + + constexpr void add( unsigned char b ) noexcept { bits[ b >> 3 ] |= std::uint8_t( 1u << ( b & 7u ) ); } + constexpr void addRange( unsigned char lo, unsigned char hi ) noexcept + { + for( unsigned b = lo; b <= unsigned( hi ); ++b ) + { + add( static_cast( b ) ); + } + } + constexpr bool contains( unsigned char b ) const noexcept { return ( bits[ b >> 3 ] >> ( b & 7u ) ) & 1u; } +}; + +// The scalar oracle deliberately uses a DIFFERENT representation of the same set — four u64 words, tested +// with a shift — so a bug in the (b >> 3, b & 7) packing cannot hide behind an oracle that shares it. +inline std::size_t findByteset_scalar( const char* p, std::size_t n, const Byteset256& set ) noexcept +{ + std::uint64_t words[ 4 ] = { 0, 0, 0, 0 }; + for( unsigned b = 0; b < 256u; ++b ) + { + if( set.contains( static_cast( b ) ) ) + { + words[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); + } + } + for( std::size_t k = 0; k < n; ++k ) + { + const unsigned char c = static_cast( p[ k ] ); + if( ( words[ c >> 6 ] >> ( c & 63u ) ) & 1u ) + { + return k; + } + } + return n; +} + +inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& set ) noexcept +{ + std::size_t k = 0; +#if defined( __ARM_NEON ) + static constexpr std::uint8_t kBitOfIndex[ 16 ] = { 1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128 }; + const uint8x16_t rowsLo = vld1q_u8( set.bits ); // rows 0..15 → bytes 0x00..0x7F + const uint8x16_t rowsHi = vld1q_u8( set.bits + 16 ); // rows 16..31 → bytes 0x80..0xFF + const uint8x16_t bitTab = vld1q_u8( kBitOfIndex ); + for( ; k + 16 <= n; k += 16 ) + { + const uint8x16_t v = vld1q_u8( reinterpret_cast( p + k ) ); + const uint8x16_t idx = vshrq_n_u8( v, 3 ); // 0..31 + // vqtbl1q_u8 returns 0 for an index >= 16, which is exactly the lane selection we want: the low + // table answers for idx 0..15 and zeroes the rest, the high table answers for idx 16..31 after + // the (wrapping) subtract pushes 0..15 far out of range. OR them and there is no blend to do. +#if defined( STRKERN_MUTATE ) + const uint8x16_t row = vqtbl1q_u8( rowsLo, idx ); // MUTATION: high half dropped +#else + const uint8x16_t row = vorrq_u8( vqtbl1q_u8( rowsLo, idx ), + vqtbl1q_u8( rowsHi, vsubq_u8( idx, vdupq_n_u8( 16 ) ) ) ); +#endif + const uint8x16_t bit = vqtbl1q_u8( bitTab, vandq_u8( v, vdupq_n_u8( 7 ) ) ); + const std::uint64_t m = neonNibbleMask( vtstq_u8( row, bit ) ); + if( m != 0 ) + { + return k + ( std::size_t( __builtin_ctzll( m ) ) >> 2 ); + } + } +#elif defined( __AVX2__ ) + static constexpr std::uint8_t kBitOfIndex[ 16 ] = { 1, 2, 4, 8, 16, 32, 64, 128, 1, 2, 4, 8, 16, 32, 64, 128 }; + const __m256i rowsLo = _mm256_broadcastsi128_si256( _mm_loadu_si128( reinterpret_cast( set.bits ) ) ); + const __m256i rowsHi = _mm256_broadcastsi128_si256( _mm_loadu_si128( reinterpret_cast( set.bits + 16 ) ) ); + const __m256i bitTab = _mm256_broadcastsi128_si256( _mm_loadu_si128( reinterpret_cast( kBitOfIndex ) ) ); + for( ; k + 32 <= n; k += 32 ) + { + const __m256i v = _mm256_loadu_si256( reinterpret_cast( p + k ) ); + const __m256i idx = _mm256_and_si256( _mm256_srli_epi16( v, 3 ), _mm256_set1_epi8( 0x1F ) ); + // pshufb takes index & 15 (it only zeroes when bit 7 is set), so unlike NEON the two halves must + // be BLENDED rather than OR-ed — and the selector is v itself: its high bit is set exactly for + // the bytes 0x80..0xFF whose row lives in the high table. +#if defined( STRKERN_MUTATE ) + const __m256i row = _mm256_shuffle_epi8( rowsLo, idx ); // MUTATION: high half dropped +#else + const __m256i row = _mm256_blendv_epi8( _mm256_shuffle_epi8( rowsLo, idx ), + _mm256_shuffle_epi8( rowsHi, idx ), v ); +#endif + const __m256i bit = _mm256_shuffle_epi8( bitTab, _mm256_and_si256( v, _mm256_set1_epi8( 7 ) ) ); + const __m256i hit = _mm256_cmpeq_epi8( _mm256_and_si256( row, bit ), bit ); + const std::uint32_t m = std::uint32_t( _mm256_movemask_epi8( hit ) ); + if( m != 0 ) + { + return k + std::size_t( __builtin_ctz( m ) ); + } + } +#endif + const std::size_t tail = findByteset_scalar( p + k, n - k, set ); + return tail == n - k ? n : k + tail; +} + +} // namespace rw::strkern diff --git a/test/binoverridecheck.sh b/test/binoverridecheck.sh index e8392497a..36268692d 100755 --- a/test/binoverridecheck.sh +++ b/test/binoverridecheck.sh @@ -110,6 +110,7 @@ EXEMPT = { "portablebuildcheck.sh": "CMake-configure-level gate only; the gate's own banner says 'no ripwire binary needed'", "qschemetripcheck.sh": "greps src/quality.h's tripwire comment against the test/*.sh manifest; pure file check", "radixsimdcheck.sh": "builds its OWN standalone harness binaries per SIMD arm, independent of build/ripwire", + "strkerncheck.sh": "builds its OWN standalone harness binaries per SIMD arm (native, mutated, x86_64 cross), independent of build/ripwire", "releaseinstallcheck.sh": "tests install.sh against a FABRICATED release asset/stub server; independent of build/ripwire", "reusefirstworkflowcheck.sh":"checks skills/ripwire-reuse-first/SKILL.md content; pure file check", "ripwirepubliccheck.sh": "checks git-tracked files for leaked private content; pure file/grep check", diff --git a/test/portablebuildcheck.sh b/test/portablebuildcheck.sh index 8cbaf6bc3..46e20b8e0 100755 --- a/test/portablebuildcheck.sh +++ b/test/portablebuildcheck.sh @@ -39,21 +39,27 @@ command -v cmake >/dev/null 2>&1 || { echo "no cmake on PATH"; exit 2; } [ -f "$MODULE" ] || { echo "missing $MODULE"; exit 2; } # Build a tiny standalone project dir that only includes PortableFlags.cmake and prints its result. +# $2, when given, overrides CMAKE_SYSTEM_PROCESSOR before the include — the only way this +# Apple-Silicon machine can drive the x86-64 branch of the real module (LANGUAGES NONE means no +# toolchain probe runs afterwards to overwrite it). mk_probe(){ local dir="$1" + local proc="${2:-}" mkdir -p "$dir/src" - cat >"$dir/CMakeLists.txt" <"$dir/CMakeLists.txt" } # Configure the probe with the given extra -D args; echoes the RIPWIRE_ARCH_FLAGS line (without the # 'RIPWIRE_ARCH_FLAGS:' prefix), or nothing if the configure failed outright. run_probe(){ local dir="$1"; shift - mk_probe "$dir" + local proc="${PROBE_PROC:-}" + mk_probe "$dir" "$proc" local log="$dir/configure.log" if ! cmake -S "$dir" -B "$dir/build" "$@" >"$log" 2>&1; then printf 'CONFIGURE_FAILED\n' @@ -93,6 +99,32 @@ else no "RIPWIRE_PRETEND_LINUX=ON flags missing expected portable baseline: '$linuxFlags'" fi +# ── #2b: the x86-64 FLOOR — an x86-64 target MUST carry -march=x86-64-v3 ────────────────────────────── +# Not cosmetic: src/infra/strkern.h compiles its AVX2 mirror behind __AVX2__, which only this flag defines +# on a portable build. Drop the flag and every x86-64 binary silently falls back to the scalar twins — +# correct output, several times the CPU on every text-scanning verb, and nothing else in the tree notices. +# The owner's floor is v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE, the RHEL 10 level), never v4/AVX-512. +x86Flags="$( PROBE_PROC=x86_64 run_probe "$TMP/x86" -DRIPWIRE_PRETEND_LINUX=ON )" +if [ "$x86Flags" = "CONFIGURE_FAILED" ]; then + no "#2b x86-64 probe configure failed outright: $(tail -5 "$TMP/x86/configure.log" 2>/dev/null)" +elif ! printf '%s' "$x86Flags" | grep -q -- '-march=x86-64-v3'; then + no "#2b an x86-64 target did NOT get the -march=x86-64-v3 floor (strkern.h's AVX2 path would not compile): '$x86Flags'" +elif printf '%s' "$x86Flags" | grep -qE -- '-march=x86-64-v4|-mavx512'; then + no "#2b x86-64 floor was raised to v4/AVX-512, which the owner decision excludes: '$x86Flags'" +elif printf '%s' "$x86Flags" | grep -q -- '-mcpu=apple-m1'; then + no "#2b x86-64 target also emits -mcpu=apple-m1: '$x86Flags'" +else + ok "#2b x86-64 target carries the v3 floor and nothing Apple-specific: '$x86Flags'" +fi + +# ── #2c: the floor is x86-ONLY — an aarch64 Linux target must not be handed an x86 -march ───────────── +armFlags="$( PROBE_PROC=aarch64 run_probe "$TMP/arm" -DRIPWIRE_PRETEND_LINUX=ON )" +if printf '%s' "$armFlags" | grep -q -- '-march=x86'; then + no "#2c an aarch64 target was handed an x86 architecture flag: '$armFlags'" +else + ok "#2c aarch64 target stays generic (NEON is baseline there, no flag needed): '$armFlags'" +fi + # ── #3: RIPWIRE_NATIVE=ON stays opt-in and unaffected by the pretend-Linux hook ───────────────────────── nativeFlags="$( run_probe "$TMP/native" -DRIPWIRE_NATIVE=ON -DRIPWIRE_PRETEND_LINUX=ON )" if printf '%s' "$nativeFlags" | grep -q -- '-march=native'; then diff --git a/test/regression.sh b/test/regression.sh index 0d54307d8..290158b3e 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" diff --git a/test/strkern_harness.cpp b/test/strkern_harness.cpp new file mode 100644 index 000000000..d2ab779e2 --- /dev/null +++ b/test/strkern_harness.cpp @@ -0,0 +1,690 @@ +// strkern_harness.cpp — SIMD-vs-scalar parity gate for src/infra/strkern.h, plus the tokenizer +// equivalence arm that pins lexindex.h's mask-driven walkers against the byte-at-a-time state machines +// they replaced. +// +// A classMasks — per-byte [A-Z]/[a-z]/[0-9]/alnum bitmasks over one block, vector path vs +// the range-test oracle, for EVERY length 0..kBlockBytes. +// B lowerFoldAscii — in-place A-Z fold, vector vs SWAR-scalar, on buffers that straddle every +// block boundary. +// C lowerFoldedEquals — folded compare vs the scalar twin, including every single-byte difference +// position and the case-only differences that are the point of the kernel. +// D findByte / find3 — first-occurrence scans vs the scalar twins AND vs a naive memchr/memcmp +// oracle, needle present and absent, matches at 0 / at n-1 / straddling. +// E findByteset — 256-bit set scan; sets built to hit the (b>>3, b&7) packing's seams +// (empty, full, the 0x80 boundary, one byte only, the XML-escape set). +// F tokenizer equivalence — forEachLexSubtoken / forEachLexSubtokenHashed as shipped vs VERBATIM +// copies of the pre-2026-09-10 byte-at-a-time walkers kept in this file. +// Every (start, end) span and every fused hash must be identical, over the +// random corpus AND over every byte of src/ and docs/. +// +// Corpora: (1) a fixed-seed random sweep — 100k buffers, lengths 0..300, drawn from four alphabets +// (identifier-ish, full ASCII, high-bit/UTF-8, and a camel/acronym-dense generator that manufactures the +// exact seams the tokenizer rule turns on); (2) every regular file under src/ and docs/ of the repo root +// given as argv[1], read whole. Real text is not optional here: the random arms cannot produce the +// distribution of `ACRONYMWord`, `snake_case` and `//` runs that the shipped rule was tuned on. +// +// NON-VACUITY: the banner prints the compiled path (`strkern path: NEON|AVX2|scalar`). On arm64/x86_64 the +// gate script REQUIRES a vector path — a scalar-only build there would compare the oracle to itself. +// CAN-GO-RED: compiling with -DSTRKERN_MUTATE=1 perturbs the SIMD tables only; the gate script proves the +// harness fails under it, so a green run means the parity assertions actually bind. +// +// Exit 0 = all pass; nonzero = failure. + +#include "../src/infra/strkern.h" +#include "../src/lexindex.h" +#include "harnesscommon.h" // checkf / g_fail / DeterministicRng — shared with the other SIMD harnesses + +#include +#include +#include +#include +#include +#include + +namespace sk = rw::strkern; + +// ============================================================================ +// corpora +// ============================================================================ + +// four alphabets, each aimed at a different failure mode +enum class Alphabet +{ + Identifier, // [A-Za-z0-9_] — the tokenizer's natural food + FullAscii, // 0x00..0x7F — every separator, every nibble-table seam ('@' '[' '`' '{' ':' '/') + HighBit, // 0x00..0xFF — proves the >= 0x80 half is a separator and never folds + CamelDense // manufactured camel / ACRONYMWord / digit seams at high density +}; + +static void drawBuffer( DeterministicRng& gen, Alphabet alpha, std::size_t n, std::string& out ) +{ + static const char kIdent[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + out.clear(); + out.reserve( n ); + while( out.size() < n ) + { + const std::uint64_t r = gen.next(); + switch( alpha ) + { + case Alphabet::Identifier: + out.push_back( kIdent[ r % ( sizeof( kIdent ) - 1 ) ] ); + break; + case Alphabet::FullAscii: + out.push_back( char( r & 0x7F ) ); + break; + case Alphabet::HighBit: + out.push_back( char( r & 0xFF ) ); + break; + case Alphabet::CamelDense: + { + // a short run of one class, then switch — so seams land every 1..4 bytes + const int kind = int( r & 3 ); + const std::size_t runLen = 1 + std::size_t( ( r >> 2 ) & 3 ); + for( std::size_t j = 0; j < runLen && out.size() < n; ++j ) + { + const std::uint64_t s = gen.next(); + switch( kind ) + { + case 0: out.push_back( char( 'A' + ( s % 26 ) ) ); break; + case 1: out.push_back( char( 'a' + ( s % 26 ) ) ); break; + case 2: out.push_back( char( '0' + ( s % 10 ) ) ); break; + default: out.push_back( ( s & 1 ) ? '_' : ' ' ); break; + } + } + break; + } + } + } + out.resize( n ); +} + +// every regular file under /src and /docs, read whole +static void loadRepoText( const char* root, std::vector& outFiles, std::vector& outNames ) +{ + for( const char* sub : { "src", "docs" } ) + { + const std::filesystem::path dir = std::filesystem::path( root ) / sub; + std::error_code ec; + if( !std::filesystem::is_directory( dir, ec ) ) + { + continue; + } + for( std::filesystem::recursive_directory_iterator it( dir, ec ), end; it != end && !ec; it.increment( ec ) ) + { + if( !it->is_regular_file( ec ) ) + { + continue; + } + std::FILE* fp = std::fopen( it->path().string().c_str(), "rb" ); + if( fp == nullptr ) + { + continue; + } + std::string bytes; + char buf[ 65536 ]; + std::size_t got = 0; + while( ( got = std::fread( buf, 1, sizeof( buf ), fp ) ) > 0 ) + { + bytes.append( buf, got ); + } + std::fclose( fp ); + outFiles.push_back( std::move( bytes ) ); + outNames.push_back( it->path().string() ); + } + } +} + +// ============================================================================ +// Arm A — classMasks +// ============================================================================ + +static bool masksEqual( const sk::Masks& a, const sk::Masks& b ) +{ + return a.alnum == b.alnum && a.upper == b.upper && a.lower == b.lower && a.digit == b.digit; +} + +// Every length 0..kBlockBytes over one buffer, vector vs oracle. Returns the first failing (length, +// offset) as a message, or an empty string. +static std::string classMasksSweep( const std::string& text ) +{ + for( std::size_t off = 0; off < text.size(); ++off ) + { + const std::size_t avail = text.size() - off; + const std::size_t maxN = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; + for( std::size_t n = 0; n <= maxN; ++n ) + { + sk::Masks got{}, want{}; + sk::classMasks( text.data() + off, n, got ); + sk::classMasks_scalar( text.data() + off, n, want ); + if( !masksEqual( got, want ) ) + { + char msg[ 256 ]; + std::snprintf( msg, sizeof( msg ), + "off=%zu n=%zu got(a=%08x u=%08x l=%08x d=%08x) want(a=%08x u=%08x l=%08x d=%08x)", + off, n, got.alnum, got.upper, got.lower, got.digit, + want.alnum, want.upper, want.lower, want.digit ); + return msg; + } + } + } + return {}; +} + +// the whole byte alphabet, one byte per position, so no class boundary can go unvisited +static void armAllBytes() +{ + std::string every; + for( unsigned b = 0; b < 256u; ++b ) + { + every.push_back( char( b ) ); + } + const std::string fail = classMasksSweep( every ); + checkf( fail.empty(), "A1 classMasks over all 256 byte values, every offset and length%s%s", + fail.empty() ? "" : " — ", fail.c_str() ); + + // the class definition itself, one byte at a time, against the shipped lexindex predicate set + bool defOk = true; + for( unsigned b = 0; b < 256u; ++b ) + { + const char c = char( b ); + sk::Masks m{}; + sk::classMasks( &c, 1, m ); + const bool wantUpper = b >= 'A' && b <= 'Z'; + const bool wantLower = b >= 'a' && b <= 'z'; + const bool wantDigit = b >= '0' && b <= '9'; + defOk = defOk && ( ( m.upper & 1u ) != 0 ) == wantUpper && ( ( m.lower & 1u ) != 0 ) == wantLower + && ( ( m.digit & 1u ) != 0 ) == wantDigit + && ( ( m.alnum & 1u ) != 0 ) == ( wantUpper || wantLower || wantDigit ); + } + checkf( defOk, "A2 classMasks single-byte classes match [A-Z]/[a-z]/[0-9] exactly (bytes >= 0x80 are separators)" ); +} + +// ============================================================================ +// Arm F — the tokenizer, and the VERBATIM pre-change walkers it must equal +// ============================================================================ + +// Kept byte-for-byte as they stood at 05f4b892 (src/lexindex.h:130 and :201) so this arm compares the new +// mask-driven walkers against the OLD code, not against a paraphrase of it. Do not "clean these up". + +template< class EmitFn > +static void refForEachLexSubtoken( std::string_view text, EmitFn&& emit ) +{ + constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); + std::size_t tokStartByte = kNoTokenByte; + bool prevUpper = false; + for( std::size_t k = 0; k < text.size(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( text[ k ] ); + const bool upper = c >= 'A' && c <= 'Z'; + const bool lower = c >= 'a' && c <= 'z'; + const bool digit = c >= '0' && c <= '9'; + if( !upper && !lower && !digit ) + { + if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k ); tokStartByte = kNoTokenByte; } + prevUpper = false; + continue; + } + if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + { + emit( tokStartByte, k ); + tokStartByte = k; + } + if( tokStartByte == kNoTokenByte ) + { + tokStartByte = k; + } + prevUpper = upper; + } + if( tokStartByte != kNoTokenByte ) + { + emit( tokStartByte, text.size() ); + } +} + +template< class EmitFn > +static void refForEachLexSubtokenHashed( std::string_view text, EmitFn&& emit ) +{ + constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); + constexpr std::uint64_t kFnvBasis = 1469598103934665603ull; + std::size_t tokStartByte = kNoTokenByte; + std::uint64_t h = kFnvBasis; + bool prevUpper = false; + const auto mix = [ & ]( unsigned char c ) noexcept { h = rw::hashutil::fnv1aAbsorb( h, char( rw::lexLowerByte( c ) ) ); }; + const auto beginToken = [ & ]( unsigned char c, std::size_t k ) noexcept + { + tokStartByte = k; + h = kFnvBasis; + mix( c ); + }; + for( std::size_t k = 0; k < text.size(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( text[ k ] ); + const bool upper = c >= 'A' && c <= 'Z'; + const bool lower = c >= 'a' && c <= 'z'; + const bool digit = c >= '0' && c <= '9'; + if( !upper && !lower && !digit ) + { + if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k, h ); tokStartByte = kNoTokenByte; } + prevUpper = false; + continue; + } + if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + { + emit( tokStartByte, k, h ); + beginToken( c, k ); + prevUpper = true; + continue; + } + if( tokStartByte == kNoTokenByte ) { beginToken( c, k ); prevUpper = upper; continue; } + mix( c ); + prevUpper = upper; + } + if( tokStartByte != kNoTokenByte ) + { + emit( tokStartByte, text.size(), h ); + } +} + +struct Tok +{ + std::size_t start = 0; + std::size_t end = 0; + std::uint64_t hash = 0; +}; + +static void collectRef( std::string_view text, std::vector< Tok >& out ) +{ + out.clear(); + refForEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) + { + out.push_back( { s, e, h } ); + } ); +} + +static void collectNew( std::string_view text, std::vector< Tok >& out ) +{ + out.clear(); + rw::forEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) + { + out.push_back( { s, e, h } ); + } ); +} + +// spans only (the hash-free walker) — a separate list, because the two shipped walkers are separate code +static void collectRefSpans( std::string_view text, std::vector< Tok >& out ) +{ + out.clear(); + refForEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); +} + +static void collectNewSpans( std::string_view text, std::vector< Tok >& out ) +{ + out.clear(); + rw::forEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); +} + +// Compare all four lists for one text. Returns "" when identical, else the first divergence. +static std::string tokenizerDiff( std::string_view text ) +{ + static std::vector< Tok > refH, newH, refS, newS; + collectRef( text, refH ); + collectNew( text, newH ); + collectRefSpans( text, refS ); + collectNewSpans( text, newS ); + + char msg[ 384 ]; + if( refS.size() != newS.size() ) + { + std::snprintf( msg, sizeof( msg ), "span COUNT %zu vs %zu (len=%zu)", refS.size(), newS.size(), text.size() ); + return msg; + } + for( std::size_t i = 0; i < refS.size(); ++i ) + { + if( refS[ i ].start != newS[ i ].start || refS[ i ].end != newS[ i ].end ) + { + std::snprintf( msg, sizeof( msg ), "span #%zu [%zu,%zu) vs [%zu,%zu) (len=%zu)", i, + refS[ i ].start, refS[ i ].end, newS[ i ].start, newS[ i ].end, text.size() ); + return msg; + } + } + if( refH.size() != newH.size() ) + { + std::snprintf( msg, sizeof( msg ), "hashed COUNT %zu vs %zu (len=%zu)", refH.size(), newH.size(), text.size() ); + return msg; + } + for( std::size_t i = 0; i < refH.size(); ++i ) + { + if( refH[ i ].start != newH[ i ].start || refH[ i ].end != newH[ i ].end || refH[ i ].hash != newH[ i ].hash ) + { + std::snprintf( msg, sizeof( msg ), "hashed #%zu [%zu,%zu)#%016llx vs [%zu,%zu)#%016llx (len=%zu)", i, + refH[ i ].start, refH[ i ].end, ( unsigned long long )refH[ i ].hash, + newH[ i ].start, newH[ i ].end, ( unsigned long long )newH[ i ].hash, text.size() ); + return msg; + } + // and the fused hash must still equal the standalone lexSubtokenHash of the same span + const std::uint64_t standalone = rw::lexSubtokenHash( text.data() + newH[ i ].start, newH[ i ].end - newH[ i ].start ); + if( standalone != newH[ i ].hash ) + { + std::snprintf( msg, sizeof( msg ), "fused hash #%zu %016llx != lexSubtokenHash %016llx", i, + ( unsigned long long )newH[ i ].hash, ( unsigned long long )standalone ); + return msg; + } + // ... and the hash-free walker's spans must be the same spans + if( refS[ i ].start != newH[ i ].start || refS[ i ].end != newH[ i ].end ) + { + std::snprintf( msg, sizeof( msg ), "walker disagreement #%zu [%zu,%zu) vs [%zu,%zu)", i, + refS[ i ].start, refS[ i ].end, newH[ i ].start, newH[ i ].end ); + return msg; + } + } + return {}; +} + +// The hand-written seam table from docs/EVALS.md §4 — the cases the acronym rule exists for, spelled out +// so a failure names the input rather than a random offset. +static void armTokenizerSeams() +{ + static const char* kCases[] = { + "", "a", "A", "aB", "Ab", "AB", "ABc", "aBc", "MCP", "MCP2Server", "HTTPServer", "IOError", + "XMLHttpRequest", "_max_speed", "updateCollisionPositionVelocity", "foo bar", " ", "__", + "A1B2C3", "camelCASE", "CASEcamel", "endsWithUPPER", "x", "0", "9a", "a9", "Z", "aZ", "ZZa", + "ZZZZZZZZZZZZZZZZZZZZa", // acronym run straddling a 16-byte block + "aaaaaaaaaaaaaaaBcccccccccccccccDeeeeeeeeeeeeeeeF", // camel seam at 15/31/47 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaBc", // camel seam exactly at 32 + "ABCDEFGHIJKLMNOPa", "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFa", // acronym seam at 16 and at 32 + "____________________abc", "abc____________________", + }; + bool ok = true; + std::string firstFail; + for( const char* c : kCases ) + { + const std::string d = tokenizerDiff( c ); + if( !d.empty() && firstFail.empty() ) + { + firstFail = std::string( "\"" ) + c + "\": " + d; + ok = false; + } + } + checkf( ok, "F1 tokenizer equals the pre-change walker on the %zu registered seam cases%s%s", + sizeof( kCases ) / sizeof( kCases[ 0 ] ), ok ? "" : " — ", firstFail.c_str() ); +} + +// ============================================================================ +// main +// ============================================================================ + +int main( int argc, char** argv ) +{ + const char* root = argc > 1 ? argv[ 1 ] : "."; + std::printf( "strkern: path=%s block=%zu root=%s\n", sk::kPathName, sk::kBlockBytes, root ); + std::printf( "strkern path: %s\n", sk::kPathName ); + + armAllBytes(); + armTokenizerSeams(); + + // ── the fixed-seed random sweep ────────────────────────────────────────────────────────────────── + DeterministicRng gen{ 0x5DEECE66Dull }; + std::string buf, folded, foldedRef, lowered; + std::string classFail, foldFail, eqFail, findFail, tokFail; + std::size_t bufferCount = 0; + + // one byteset per shape the (b>>3, b&7) packing could get wrong + sk::Byteset256 setEmpty, setFull, setOne, setHighOnly, setXml; + for( unsigned b = 0; b < 256u; ++b ) + { + setFull.add( static_cast< unsigned char >( b ) ); + } + setOne.add( 'q' ); + setHighOnly.addRange( 0x80, 0xFF ); + for( char c : { '&', '<', '>', '"', '\'', '\t', '\n', '\r' } ) + { + setXml.add( static_cast< unsigned char >( c ) ); + } + setXml.addRange( 0x80, 0xFF ); + const sk::Byteset256* kSets[] = { &setEmpty, &setFull, &setOne, &setHighOnly, &setXml }; + const char* kSetNames[] = { "empty", "full", "one", "high", "xml" }; + + for( int iter = 0; iter < 100000; ++iter ) + { + const Alphabet alpha = Alphabet( iter & 3 ); + const std::size_t n = std::size_t( gen.next() % 301u ); // 0..300, straddles 16/32 repeatedly + drawBuffer( gen, alpha, n, buf ); + ++bufferCount; + + // A — classMasks at every offset/length that fits in one block, but only on a slice (the full + // O(n * block) sweep on 100k buffers would dominate the gate's runtime) + if( classFail.empty() ) + { + const std::size_t probeOff = n == 0 ? 0 : std::size_t( gen.next() % n ); + const std::size_t avail = n - probeOff; + const std::size_t maxN = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; + for( std::size_t m = 0; m <= maxN && classFail.empty(); ++m ) + { + sk::Masks got{}, want{}; + sk::classMasks( buf.data() + probeOff, m, got ); + sk::classMasks_scalar( buf.data() + probeOff, m, want ); + if( !masksEqual( got, want ) ) + { + char msg[ 256 ]; + std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d off=%zu n=%zu", iter, int( alpha ), probeOff, m ); + classFail = msg; + } + } + } + + // B — lowerFoldAscii, vector vs SWAR scalar, in place + if( foldFail.empty() ) + { + folded = buf; + foldedRef = buf; + sk::lowerFoldAscii( folded.data(), folded.size() ); + sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); + if( folded != foldedRef ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d n=%zu", iter, int( alpha ), n ); + foldFail = msg; + } + // and against the definition, byte by byte + for( std::size_t k = 0; k < n && foldFail.empty(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( buf[ k ] ); + const unsigned char want = ( c >= 'A' && c <= 'Z' ) ? static_cast< unsigned char >( c + 0x20 ) : c; + if( static_cast< unsigned char >( folded[ k ] ) != want ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "definition iter=%d k=%zu byte=%02x", iter, k, c ); + foldFail = msg; + } + } + } + + // C — lowerFoldedEquals: equal case, and every single-byte perturbation of one random position + if( eqFail.empty() && n > 0 ) + { + lowered = buf; + sk::lowerFoldAscii_scalar( lowered.data(), lowered.size() ); + if( !sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) + || !sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) + { + eqFail = "self-compare returned false"; + } + const std::size_t at = std::size_t( gen.next() % n ); + const char old = lowered[ at ]; + lowered[ at ] = char( static_cast< unsigned char >( old ) ^ 0x01 ); + if( eqFail.empty() + && sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) != sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "perturbed iter=%d at=%zu n=%zu", iter, at, n ); + eqFail = msg; + } + lowered[ at ] = old; + } + + // D/E — the find kernels vs their scalar twins vs a naive oracle + if( findFail.empty() ) + { + const char needle = char( gen.next() & 0xFF ); + const std::size_t gotB = sk::findByte( buf.data(), n, needle ); + const std::size_t refB = sk::findByte_scalar( buf.data(), n, needle ); + std::size_t naive = n; + for( std::size_t k = 0; k < n; ++k ) + { + if( buf[ k ] == needle ) { naive = k; break; } + } + if( gotB != refB || gotB != naive ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "findByte iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, gotB, refB, naive, n ); + findFail = msg; + } + + // find3: half the time plant the needle so a HIT is exercised, half the time draw at random + char needle3[ 3 ] = { char( gen.next() & 0xFF ), char( gen.next() & 0xFF ), char( gen.next() & 0xFF ) }; + if( n >= 3 && ( gen.next() & 1 ) ) + { + const std::size_t at = std::size_t( gen.next() % ( n - 2 ) ); + std::memcpy( needle3, buf.data() + at, 3 ); + } + const std::size_t got3 = sk::find3( buf.data(), n, needle3 ); + const std::size_t ref3 = sk::find3_scalar( buf.data(), n, needle3 ); + std::size_t nai3 = n; + for( std::size_t k = 0; k + 3 <= n; ++k ) + { + if( std::memcmp( buf.data() + k, needle3, 3 ) == 0 ) { nai3 = k; break; } + } + if( findFail.empty() && ( got3 != ref3 || got3 != nai3 ) ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "find3 iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, got3, ref3, nai3, n ); + findFail = msg; + } + + const std::size_t si = std::size_t( gen.next() % 5u ); + const std::size_t gotS = sk::findByteset( buf.data(), n, *kSets[ si ] ); + const std::size_t refS = sk::findByteset_scalar( buf.data(), n, *kSets[ si ] ); + std::size_t naiS = n; + for( std::size_t k = 0; k < n; ++k ) + { + if( kSets[ si ]->contains( static_cast< unsigned char >( buf[ k ] ) ) ) { naiS = k; break; } + } + if( findFail.empty() && ( gotS != refS || gotS != naiS ) ) + { + char msg[ 192 ]; + std::snprintf( msg, sizeof( msg ), "findByteset[%s] iter=%d got=%zu ref=%zu naive=%zu n=%zu", + kSetNames[ si ], iter, gotS, refS, naiS, n ); + findFail = msg; + } + } + + // F — tokenizer equivalence on the random corpus + if( tokFail.empty() ) + { + const std::string d = tokenizerDiff( buf ); + if( !d.empty() ) + { + char msg[ 512 ]; + std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d %s", iter, int( alpha ), d.c_str() ); + tokFail = msg; + } + } + } + + checkf( classFail.empty(), "A3 classMasks vs scalar oracle on %zu random buffers (4 alphabets, len 0..300)%s%s", + bufferCount, classFail.empty() ? "" : " — ", classFail.c_str() ); + checkf( foldFail.empty(), "B1 lowerFoldAscii vector == SWAR scalar == the A-Z definition, %zu buffers%s%s", + bufferCount, foldFail.empty() ? "" : " — ", foldFail.c_str() ); + checkf( eqFail.empty(), "C1 lowerFoldedEquals vector == scalar, equal and perturbed, %zu buffers%s%s", + bufferCount, eqFail.empty() ? "" : " — ", eqFail.c_str() ); + checkf( findFail.empty(), "D1/E1 findByte / find3 / findByteset vector == scalar == naive oracle, %zu buffers%s%s", + bufferCount, findFail.empty() ? "" : " — ", findFail.c_str() ); + checkf( tokFail.empty(), "F2 tokenizer == pre-change walker (spans + fused hashes) on %zu random buffers%s%s", + bufferCount, tokFail.empty() ? "" : " — ", tokFail.c_str() ); + + // ── the real-text corpus ───────────────────────────────────────────────────────────────────────── + std::vector< std::string > files, names; + loadRepoText( root, files, names ); + checkf( files.size() >= 50, "G0 real-text corpus loaded: %zu files under %s/{src,docs} (need >= 50 for the arm to mean anything)", + files.size(), root ); + + std::string realClassFail, realFoldFail, realTokFail, realFindFail; + std::size_t totalBytes = 0; + for( std::size_t fi = 0; fi < files.size(); ++fi ) + { + const std::string& text = files[ fi ]; + totalBytes += text.size(); + + if( realClassFail.empty() ) + { + // every block-aligned window plus the ragged tail — the whole file's bytes are classified + for( std::size_t off = 0; off < text.size() && realClassFail.empty(); off += sk::kBlockBytes ) + { + const std::size_t avail = text.size() - off; + const std::size_t m = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; + sk::Masks got{}, want{}; + sk::classMasks( text.data() + off, m, got ); + sk::classMasks_scalar( text.data() + off, m, want ); + if( !masksEqual( got, want ) ) + { + realClassFail = names[ fi ] + " @" + std::to_string( off ); + } + } + } + if( realFoldFail.empty() ) + { + folded = text; + foldedRef = text; + sk::lowerFoldAscii( folded.data(), folded.size() ); + sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); + if( folded != foldedRef ) + { + realFoldFail = names[ fi ]; + } + else if( !sk::lowerFoldedEquals( text.data(), folded.data(), text.size() ) ) + { + realFoldFail = names[ fi ] + " (foldedEquals)"; + } + } + if( realFindFail.empty() && text.size() >= 3 ) + { + // the needle a --grep trigram probe would use: the file's own middle three bytes + const std::size_t at = text.size() / 2 - 1; + char needle3[ 3 ]; + std::memcpy( needle3, text.data() + at, 3 ); + const std::size_t got3 = sk::find3( text.data(), text.size(), needle3 ); + const std::size_t ref3 = sk::find3_scalar( text.data(), text.size(), needle3 ); + std::size_t nai3 = text.size(); + for( std::size_t k = 0; k + 3 <= text.size(); ++k ) + { + if( std::memcmp( text.data() + k, needle3, 3 ) == 0 ) { nai3 = k; break; } + } + const std::size_t gotS = sk::findByteset( text.data(), text.size(), setXml ); + const std::size_t refS = sk::findByteset_scalar( text.data(), text.size(), setXml ); + if( got3 != ref3 || got3 != nai3 || gotS != refS ) + { + realFindFail = names[ fi ]; + } + } + if( realTokFail.empty() ) + { + const std::string d = tokenizerDiff( text ); + if( !d.empty() ) + { + realTokFail = names[ fi ] + ": " + d; + } + } + } + + checkf( realClassFail.empty(), "G1 classMasks vs oracle over every byte of src/ + docs/ (%zu files, %zu bytes)%s%s", + files.size(), totalBytes, realClassFail.empty() ? "" : " — ", realClassFail.c_str() ); + checkf( realFoldFail.empty(), "G2 lowerFoldAscii / lowerFoldedEquals over the same %zu files%s%s", + files.size(), realFoldFail.empty() ? "" : " — ", realFoldFail.c_str() ); + checkf( realFindFail.empty(), "G3 find3 / findByteset over the same %zu files%s%s", + files.size(), realFindFail.empty() ? "" : " — ", realFindFail.c_str() ); + checkf( realTokFail.empty(), "G4 tokenizer == pre-change walker over every byte of src/ + docs/ (%zu files, %zu bytes)%s%s", + files.size(), totalBytes, realTokFail.empty() ? "" : " — ", realTokFail.c_str() ); + + std::printf( "%s\n", g_fail == 0 ? "ALL PASS" : "FAILURES ABOVE" ); + return g_fail; +} diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh new file mode 100755 index 000000000..e33b78783 --- /dev/null +++ b/test/strkerncheck.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# strkerncheck.sh — SIMD-vs-scalar parity gate for src/infra/strkern.h, and the tokenizer equivalence +# gate for the mask-driven walkers in src/lexindex.h. +# +# Compiles test/strkern_harness.cpp under the FULL G1 sanitizer set and runs it against (a) 100k +# fixed-seed random buffers over four alphabets, lengths 0..300, and (b) every byte of this repo's src/ +# and docs/. The harness restates each kernel's contract as an independent scalar oracle; the shipped +# vector path (NEON on arm64, AVX2 on x86-64, the scalar twins elsewhere) must match it exactly, and the +# rewritten tokenizer must reproduce the pre-2026-09-10 byte-at-a-time walkers' spans AND fused hashes. +# +# THREE THINGS THIS GATE PROVES, in the order they can go wrong: +# 1 PARITY — vector == scalar == the definition, on random and on real text. +# 2 NON-VACUITY — on arm64 the banner must say NEON, on x86-64 AVX2. A scalar-only build on those +# arches would compare the oracle to itself and pass while proving nothing. +# 3 CAN GO RED — a second build with -DSTRKERN_MUTATE=1 flips one bit of the SIMD-only nibble table, +# narrows the fold's range by one and drops findByteset's high half. That build MUST +# fail. If it passes, the parity assertions above are not binding and this gate is +# decoration. +# +# A FOURTH, BEST-EFFORT ARM: on Apple Silicon the AVX2 path is compiled with `-arch x86_64 +# -march=x86-64-v3` and run under Rosetta 2, so the x86 mirror is exercised on this machine rather than +# only on CI's ubuntu legs. It is a SKIP, never a failure, when the SDK or Rosetta is unavailable — the +# authoritative AVX2 proof is the ubuntu-24.04 CI leg. +# +# Independent of the ripwire binary and of main.cpp (pinned in test/binoverridecheck.sh's EXEMPT dict). +# Usage: bash test/strkerncheck.sh (compiles with c++/clang++) +# CXX=clang++ bash test/strkerncheck.sh + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +CXX="${CXX:-c++}" + +# ask THIS front end how it spells C++23 (see scripts/cxxstd.sh — AppleClang 15 rejects -std=c++23) +. "$ROOT/scripts/cxxstd.sh" +CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" +HARNESS="$ROOT/test/strkern_harness.cpp" +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +ARCH="$( uname -m )" +fail=0 + +echo "strkerncheck: CXX=$CXX arch=$ARCH" + +# G1's 'integer' / float-cast groups are Clang spellings; GCC only has the address,undefined core. +# Probe THIS front end rather than guessing from its name (same posture as scripts/cxxstd.sh). +SAN="-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow" +printf 'int main(){return 0;}\n' > "$WORK/probe.cpp" 2>/dev/null || true +if ! "$CXX" $SAN -fsyntax-only "$WORK/probe.cpp" 2>/dev/null; then + SAN="-fsanitize=address,undefined" +fi + +# compile one flavour of the harness; $1 = label, remaining args = extra compile flags. Echoes the binary +# path on success, nothing on failure (the caller decides whether a compile failure is fatal). +compile_harness() +{ + local LABEL="$1"; shift + local BIN="$WORK/harness_$LABEL" + if ! "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra "$@" \ + -I"$ROOT/src/infra" -I"$ROOT/src" -I"$ROOT/third_party" \ + "$HARNESS" "$ROOT/src/infra/diagnostics.cpp" -o "$BIN" 2> "$WORK/cc_$LABEL.log"; then + return 1 + fi + printf '%s\n' "$BIN" +} + +# ── 1 + 2: the shipped path, sanitized, must pass and must not be vacuous ───────────────────────────── +# -fno-sanitize-recover=all is the linchpin: a nibble-table read one lane past the end, or an unaligned +# load the compiler was allowed to assume away, must ABORT rather than report and exit 0. +BIN="$( compile_harness main $SAN -fno-sanitize-recover=all )" +if [ -z "$BIN" ]; then + echo " FAIL harness failed to compile"; sed 's/^/ /' "$WORK/cc_main.log" | head -40; exit 2 +fi + +if ! "$BIN" "$ROOT" > "$WORK/out_main.log" 2>&1; then + echo " FAIL parity/equivalence assertion failed:" + grep -A 2 'FAIL' "$WORK/out_main.log" | sed 's/^/ /' | head -30 + exit 2 +fi +if ! grep -q '^ALL PASS$' "$WORK/out_main.log"; then + echo " FAIL harness did not reach its ALL PASS line (truncated run?)" + tail -5 "$WORK/out_main.log" | sed 's/^/ /' + exit 2 +fi +printf ' PASS %s harness arms green (%s)\n' "$( grep -c ' PASS ' "$WORK/out_main.log" )" "$( head -1 "$WORK/out_main.log" | sed 's/strkern: //' )" + +WANT="" +case "$ARCH" in + arm64|aarch64) WANT="NEON" ;; + x86_64|amd64) WANT="AVX2" ;; +esac +if [ -n "$WANT" ]; then + if grep -q "^strkern path: $WANT$" "$WORK/out_main.log"; then + ok_path="$( grep '^strkern path: ' "$WORK/out_main.log" )" + printf ' PASS non-vacuity: %s on %s\n' "$ok_path" "$ARCH" + else + echo " FAIL non-vacuity ($ARCH must compile the $WANT path; banner says '$( grep '^strkern path: ' "$WORK/out_main.log" )')" + echo " a scalar-only build here compares the oracle to itself — the parity arms prove nothing" + fail=1 + fi +fi + +# ── 3: CAN GO RED ───────────────────────────────────────────────────────────────────────────────────── +# The mutation touches ONLY code inside `#if defined( STRKERN_MUTATE )` in the SIMD branches, never the +# scalar oracle — so a red run here is the parity assertion biting, not a broken build. Sanitizers are +# off for this arm: it is expected to fail, and we want it to fail on the assertion, not on a slow abort. +REDBIN="$( compile_harness mutate -DSTRKERN_MUTATE=1 )" +if [ -z "$REDBIN" ]; then + echo " FAIL can-go-red arm failed to COMPILE (the mutation must build, then fail at runtime)" + sed 's/^/ /' "$WORK/cc_mutate.log" | head -20 + fail=1 +elif "$REDBIN" "$ROOT" > "$WORK/out_mutate.log" 2>&1; then + echo " FAIL can-go-red: -DSTRKERN_MUTATE=1 build PASSED — the parity assertions are not binding" + fail=1 +else + printf ' PASS can-go-red: -DSTRKERN_MUTATE=1 fails %s arm(s) as designed\n' "$( grep -c ' FAIL ' "$WORK/out_mutate.log" )" +fi + +# ── 4: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── +# COMMON_RULES for this round: the x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE). +# Compiled without sanitizers — the ASan runtime for a cross-arch slice is not reliably present, and this +# arm's job is to run the AVX2 kernels at all, not to re-prove memory safety the native arm already did. +if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then + if X86BIN="$( compile_harness x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then + if "$X86BIN" "$ROOT" > "$WORK/out_x86.log" 2>&1 && grep -q '^ALL PASS$' "$WORK/out_x86.log"; then + if grep -q '^strkern path: AVX2$' "$WORK/out_x86.log"; then + printf ' PASS x86_64/AVX2 mirror runs green under Rosetta 2 (%s arms)\n' "$( grep -c ' PASS ' "$WORK/out_x86.log" )" + else + echo " FAIL x86_64 slice built but did NOT compile the AVX2 path: $( grep '^strkern path: ' "$WORK/out_x86.log" )" + fail=1 + fi + else + printf ' SKIP x86_64 slice built but did not run here (no Rosetta 2, or it aborted); CI ubuntu-24.04 is the AVX2 proof: %s\n' \ + "$( tail -2 "$WORK/out_x86.log" | tr '\n' ' ' )" + fi + else + printf ' SKIP no x86_64 cross slice on this toolchain (no macOS x86_64 SDK); CI ubuntu-24.04 is the AVX2 proof\n' + fi +fi + +if [ "$fail" = 0 ]; then + echo "strkerncheck: PASS" +else + echo "strkerncheck: FAILURES ABOVE" +fi +exit "$fail" From 3eebd03d03838dd5429e008d05fddf27efc37ebe Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:00:50 -0400 Subject: [PATCH 02/73] quality(churn): SELF stops gating; what gates is two committed in-window rewrites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--quality-delta`'s short-horizon-churn kind gated on churn="self" — "this uncommitted edit modifies a line that was itself committed inside the 14-day window". On an active branch that is the agent's own footprint by construction: the symbol you wrote this week and are touching again satisfies it. Audit lane Q1 measured what that costs on twelve LANDED, reviewed, merged commits of this repo, replayed in the working-tree form an agent actually runs at a "done" checkpoint: 135 of 171 gating rows were churn="self", and the labelled precision of that population was 0% — not one row a reviewer would act on. The repo had already written the verdict down itself, in commit 65d98b76's message: "that is this branch touching the symbol twice within its window, not new debt". BOTH facets are now informational. What gates is the narrower fact the kind was always about: the edited lines were rewritten by >= kShortHorizonMinCommits (2) COMMITTED commits inside the window, the working edit never counted. That number falls out of the blame this pass ALREADY spawns — gitBlameRangeHasWindowCommit answered a bool and short-circuited at the first hot line; it now collects the distinct in-window commit shas over the same ranges, and churnEditWindowCommitCount unions them across a symbol's hunks. No new subprocess, one extra pass over blame output already being read. Blame runs on HEAD, so "not counting the working edit" is by construction, not by subtraction. WORKING-TREE REPLAY — 12 landed commits (bc517e07 03ec6f14 dfcb57ba e5f9bffc 65d98b76 4ee920e5 d7873f88 b392b29c e5b2ad8a 9e9fce14 4c24b8d7 7d5dd201), ack-free, labelled with Q1 §2d's rules: | | before | after | | rows | 266 | 266 | | gating rows | 171 | 54 | | of which short-horizon-churn | 135 | 18 | | commits that gate | 12/12 | 9/12 | | gating precision TRUE | 2% | 7% | | gating precision TRUE+chronic | 16% | 50% | | WRONG rows (gating) | 1 | 1 | Every TRUE row survives: 27 gating rows labelled TRUE or TRUE-chronic before, 27 after — 0 lost, 0 demoted (exec/qrep/cmp.py). Rows are unchanged in COUNT: this dial demotes, it never drops, so the churn information stays in the document. GATE (red-first, against the pre-change binary): test/qddialscheck.sh §1 builds one file with two multi-line functions and a history that differs only in how many committed in-window commits wrote the lines the working edit touches — once() blames to one in-window commit plus one backdated out-of-window commit, twice() to two in-window commits. The pre-change binary gates BOTH ("once() must NOT gate" FAILs, "once() should be sev=minor" FAILs); after, once() is reported sev="minor" and twice() still carries gating="1" and still fires exit 2. test/qualitykindscheck.sh §2's fixture had a ONE-LINE hot() rewritten by a single commit, which can never blame more than one commit and so could no longer exercise the gating arm at all; it is now multi-line with two in-window commits on two different lines, and §4a's "genuine thrash stays MAJOR" assertion is inverted with the reason written beside it. qualitysignalcheck, churndecaycheck, churnjoincheck, qchurnmemocheck, mergechurncheck, manifestcheck, gatecountcheck, docscommandscheck, printffmtparitycheck, xmlwellformed: PASS. docs/COMMANDS.md regenerated (docs_commands_build.py) for the one-clause legend change. Co-Authored-By: Claude Fable 5.1 --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/quality.h | 117 +++++++++++++++++++++++---------- src/verbs_quality.h | 5 +- test/qddialscheck.sh | 80 ++++++++++++++++++++++ test/qualitykindscheck.sh | 26 ++++++-- test/regression.sh | 2 +- 8 files changed, 195 insertions(+), 51 deletions(-) create mode 100755 test/qddialscheck.sh diff --git a/README.md b/README.md index dde8da76a..d157ea9f3 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-586 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +587 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 **586 gate scripts** and is the authoritative list; +`test/regression.sh` names **587 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..1b1058118 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` | 586 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 587 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) @@ -5625,7 +5625,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 **586 gate scripts**, all of which exist on disk. +naming **587 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`. @@ -6637,7 +6637,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 586. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 587. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 0647c42fe..fc5a0220f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["586 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 + ["587 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "586", "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, "587", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["586 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/quality.h b/src/quality.h index a337c6017..3d1059c88 100644 --- a/src/quality.h +++ b/src/quality.h @@ -3444,10 +3444,15 @@ inline BaselineSelection selectBaseline( const std::string& root, const std::str // // The three existing gates (file churn-hot / this diff rewrites the symbol / committed thrash evidence) // establish that a symbol IS short-horizon churn. This pass answers a NARROWER question about the CURRENT -// uncommitted edit specifically: does it MODIFY pre-existing (committed) lines that were themselves last -// touched inside the churn window (SELF — genuine thrash, keep current severity), or does it only ADD new -// lines / touch lines that predate the window (AMBIENT — the file is hot, but this particular edit isn't -// touching hot content) — sev=minor, facet churn="ambient". +// uncommitted edit specifically: how many COMMITTED commits inside the churn window last wrote the +// pre-existing lines this edit modifies. One or more ⇒ the edit touches hot content, facet churn="self"; none +// (the edit only ADDS lines, or touches lines that predate the window) ⇒ churn="ambient". +// +// Q-DIAL-1 (2026-09-10) — SEVERITY no longer follows that facet. BOTH facets are informational; what GATES is +// the count reaching kShortHorizonMinCommits, i.e. "rewritten by >= 2 COMMITTED commits inside the window, the +// working edit not counted". SELF-gates was measured at 0% precision over twelve landed commits (135 of 171 +// gating rows, audit Q1 §2b/§2d) for a structural reason: on an active branch every symbol you wrote this week +// and are touching again modifies a line you yourself committed inside the window. // // Mechanism: `git diff --unified=0 HEAD -- path` gives zero-context unified-diff hunks // ("@@ -oldStart[,oldCount] +newStart[,newCount] @@"; git omits a count of 1). A hunk with oldCount==0 is a @@ -3485,15 +3490,36 @@ inline std::string gitBlameConfigPins( const std::string& root ) return hasFile ? " -c blame.ignoreRevsFile=" + shSingleQuote( ignoreRevs ) : std::string( " -c blame.ignoreRevsFile=" ); } -// Blame `root`'s HEAD over `relPath`'s [startLine, startLine+lineCount-1] and report whether ANY line in that -// range was last committed at or after `windowCutoffEpoch` (the same cutoff basis gitFileCommitCountsInDayWindow -// and gitWindowRefSha use: HEAD's own committer epoch minus the window, never wall-clock). -inline bool gitBlameRangeHasWindowCommit( const std::string& root, const std::string& relPath, - std::uint32_t startLine, std::uint32_t lineCount, std::int64_t windowCutoffEpoch ) +// Blame `root`'s HEAD over `relPath`'s [startLine, startLine+lineCount-1] and APPEND, to `outShas`, the +// fnv1a64 of every DISTINCT commit that last wrote a line in that range at or after `windowCutoffEpoch` (the +// same cutoff basis gitFileCommitCountsInDayWindow and gitWindowRefSha use: HEAD's own committer epoch minus +// the window, never wall-clock). +// +// Q-DIAL-1 (2026-09-10) — this used to answer a BOOL ("is any line in this range hot"), which is the SELF vs +// AMBIENT question and nothing more. The churn kind's GATING question is narrower and needs a count: was this +// symbol rewritten by >= kShortHorizonMinCommits COMMITTED commits inside the window, not counting the working +// edit? One in-window commit is a single touch — the branch you are on — and gating on it made 135 of 171 +// gating rows on twelve landed commits the agent's own footprint (audit Q1 §2b/§2d). Blame runs on HEAD, so +// the uncommitted edit is excluded BY CONSTRUCTION rather than by subtraction. +// +// The accumulator is a caller-owned vector rather than a return value because one symbol spans several diff +// hunks and a commit that wrote lines in two of them must count ONCE; the caller sorts + uniques the union. +// Ordering: blame output order, which is deterministic for a fixed HEAD + path, and the caller's sort makes +// the count order-independent anyway. NO short-circuit any more (the bool arm could stop at the first hot +// line): the whole range is read, which costs the rest of ONE already-spawned blame and no extra subprocess. +// +// PORCELAIN SHAPE, and why the sha is tracked separately from the time: `git blame --porcelain` prints a +// commit's metadata (committer-time among it) only the FIRST time that commit appears; later lines from the +// same commit carry the bare " " header alone. So the header line sets the CURRENT +// sha and the committer-time line decides whether that sha counts — a repeat header with no metadata needs no +// second decision, because the sha is already in (or already out of) the set. +inline void gitBlameRangeWindowCommits( const std::string& root, const std::string& relPath, + std::uint32_t startLine, std::uint32_t lineCount, std::int64_t windowCutoffEpoch, + std::vector& outShas ) { if( startLine == 0 || lineCount == 0 ) { - return false; + return; } const std::string cmd = "git -c core.quotepath=false" + gitBlameConfigPins( root ) + " -C " + shSingleQuote( root ) + " blame --porcelain -L " + std::to_string( startLine ) + ",+" + std::to_string( lineCount ) @@ -3501,9 +3527,9 @@ inline bool gitBlameRangeHasWindowCommit( const std::string& root, const std::st std::FILE* pipe = popen( cmd.c_str(), "r" ); if( !pipe ) { - return false; + return; } - bool hot = false; + std::uint64_t curSha = 0; char buf[ 512 ]; while( std::fgets( buf, sizeof( buf ), pipe ) ) { @@ -3518,16 +3544,19 @@ inline bool gitBlameRangeHasWindowCommit( const std::string& root, const std::st && ( ln.size() == 40 || ln[40] == ' ' ); if( isHeaderSha ) { - continue; // the sha itself carries no date — wait for its committer-time line + curSha = fnv1a64( ln.substr( 0, 40 ) ); // the sha itself carries no date — wait for its committer-time line + continue; } if( ln.rfind( "committer-time ", 0 ) == 0 ) { const std::int64_t t = std::strtoll( std::string( ln.substr( 15 ) ).c_str(), nullptr, 10 ); - if( t >= windowCutoffEpoch ) { hot = true; break; } // one hot line is enough — short-circuit + if( t >= windowCutoffEpoch && curSha != 0 ) + { + outShas.push_back( curSha ); + } } } pclose( pipe ); - return hot; } // One zero-context unified-diff hunk, in the two coordinate systems the SELF test needs: the OLD-side range @@ -3544,7 +3573,7 @@ static_assert( sizeof( DiffHunk ) == 16, "DiffHunk is a 4×u32 POD" ); // P3 (r27) — the RUN-SCOPED hunk memo. `git diff --unified=0 HEAD -- ` is a pure function of (HEAD, // working tree), both FIXED for the life of one --quality-delta call (the code's own section comment says so), -// yet churnEditTouchesHotLine spawned it once PER SYMBOL: a subprocess-shim log showed EIGHT byte-identical +// yet the churn blame pass spawned it once PER SYMBOL: a subprocess-shim log showed EIGHT byte-identical // spawns for a single dirty file. Caller owns the storage (house rule — views/handles at seams, no hidden // process-global state that a second root or a second MCP request would silently share). using DiffHunkMemo = HashMap>; @@ -3611,17 +3640,26 @@ inline const std::vector& diffHunksMemoized( DiffHunkMemo& memo, const return memo.emplace( relPath, gitDiffHunksVsHead( root, relPath ) ).first->second; } -// Does the CURRENT uncommitted edit to `relPath` (vs HEAD) modify any pre-existing line that overlaps the -// symbol's current [symStart, symStart+symLoc-1] span AND was itself last committed inside the window? See the -// section comment above for the full mechanism. `symStart`/`symLoc` come straight from the working-tree -// Symbol (s.line / s.loc). `memo` is the caller-owned per-run hunk cache (P3). -inline bool churnEditTouchesHotLine( DiffHunkMemo& memo, const std::string& root, const std::string& relPath, - std::uint32_t symStart, std::uint32_t symLoc, std::int64_t windowCutoffEpoch ) +// HOW MANY DISTINCT in-window COMMITS last wrote the pre-existing lines that the CURRENT uncommitted edit to +// `relPath` (vs HEAD) modifies inside the symbol's [symStart, symStart+symLoc-1] span. See the section comment +// above for the full mechanism. `symStart`/`symLoc` come straight from the working-tree Symbol (s.line / +// s.loc). `memo` is the caller-owned per-run hunk cache (P3). +// +// Q-DIAL-1: the two facts the churn kind reads off this ONE number, so they cannot drift apart — +// >= 1 the edit touches hot content at all → churn="self" (informational; it was the GATING rule until +// 2026-09-10, and it is the agent's own edit window on any active branch); +// >= kShortHorizonMinCommits the lines were rewritten by that many COMMITTED commits inside the window, +// the working edit excluded (blame is on HEAD) → this is the rewrite-thrash the kind exists to name, +// and the only form of it that gates. +// 0 (no hunk, no git, no blame) stays AMBIENT, the degrade that never inflates severity on missing evidence. +inline std::uint32_t churnEditWindowCommitCount( DiffHunkMemo& memo, const std::string& root, const std::string& relPath, + std::uint32_t symStart, std::uint32_t symLoc, std::int64_t windowCutoffEpoch ) { if( symStart == 0 ) { - return false; + return 0; } + std::vector shas; const std::uint32_t symEnd = symStart + ( symLoc > 0 ? symLoc - 1 : 0 ); for( const DiffHunk& h : diffHunksMemoized( memo, root, relPath ) ) @@ -3654,12 +3692,11 @@ inline bool churnEditTouchesHotLine( DiffHunkMemo& memo, const std::string& root continue; // this hunk falls outside the symbol } - if( gitBlameRangeHasWindowCommit( root, relPath, h.oldStart, h.oldCount, windowCutoffEpoch ) ) - { - return true; // one hot line is enough — short-circuit - } + gitBlameRangeWindowCommits( root, relPath, h.oldStart, h.oldCount, windowCutoffEpoch, shas ); } - return false; + std::sort( shas.begin(), shas.end() ); + shas.erase( std::unique( shas.begin(), shas.end() ), shas.end() ); // a commit spanning two hunks of one symbol counts ONCE + return std::uint32_t( shas.size() ); } // one reported regression (something the change made WORSE). @@ -5860,7 +5897,7 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // B10.2d — SELF-vs-AMBIENT window cutoff, same basis as gates 1/3 (HEAD's own committer epoch // minus the window, never wall-clock). A failed lookup (should not happen here since refOk // already proved resolvable history, but kept defensive) leaves churnCutoffEpoch==0, which - // degrades every symbol below to AMBIENT (churnEditTouchesHotLine is gated on `> 0`). + // degrades every symbol below to AMBIENT (churnEditWindowCommitCount is gated on `> 0`). std::int64_t churnCutoffEpoch = 0; { const std::string epochStr = gitOneLine( std::string( root ), "log -1 --format=%ct HEAD 2>/dev/null" ); @@ -5928,13 +5965,23 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap } // B10.2d: SELF vs AMBIENT — does THIS diff modify a pre-existing line that was itself - // last committed inside the window? See the section comment above churnEditTouchesHotLine. - const bool self = churnCutoffEpoch > 0 - && churnEditTouchesHotLine( churnHunkMemo, std::string( root ), - std::string( relForHash( ing.files[ s.fileId ], root ) ), - s.line, s.loc, churnCutoffEpoch ); + // last committed inside the window? See the section comment above churnEditWindowCommitCount. + // Q-DIAL-1 — ONE blame-derived number decides both the facet and the severity (see + // churnEditWindowCommitCount): >=1 in-window commit on the edited lines is SELF, and it + // is now INFORMATIONAL exactly as AMBIENT already was; >= kShortHorizonMinCommits is the + // rewrite-thrash that gates. Measured on twelve LANDED commits of this repo (audit Q1 + // §2b): the old "SELF gates" rule fired 135 of 171 gating rows, 0% of them a finding a + // reviewer would act on, because on an active branch the symbol you wrote this week and + // are touching again is churn="self" by construction. + const std::uint32_t windowCommits = churnCutoffEpoch > 0 + ? churnEditWindowCommitCount( churnHunkMemo, std::string( root ), + std::string( relForHash( ing.files[ s.fileId ], root ) ), + s.line, s.loc, churnCutoffEpoch ) + : 0u; + const bool self = windowCommits > 0; + const bool gates = windowCommits >= kShortHorizonMinCommits; regs.push_back( { "short-horizon-churn", g.canonId[i], 0, commitCounts[ s.fileId ], key, - !self, self ? "self" : "ambient", false } ); // now = window commit count on the file; origin: ALWAYS preexisting (gate 2 above required a baseline body) + !gates, self ? "self" : "ambient", false } ); // now = window commit count on the file; origin: ALWAYS preexisting (gate 2 above required a baseline body) stampLoc( i ); } } diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 38db4f8a6..145d25329 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -618,7 +618,10 @@ inline constexpr const char* kQdRowLegend = "the numeric kinds; p=\"path:line\" is the locator (root-relative; the first-sorting member for the " "clone kinds; omitted, never faked, when none resolves). churn= and surface= are per-kind " "classification facets (short-horizon-churn's self/ambient split; api-surface's new-symbol/" - "contract-change tier). Every row the header's gating= counter counts also carries a gating attribute " + "contract-change tier). BOTH churn facets are informational: what gates that kind is a symbol whose " + "edited lines were rewritten by 2 or more COMMITTED commits inside the window, the working edit never " + "counted, so churn=\"self\" alone reports that this edit touches hot content and stops there. " + "Every row the header's gating= counter counts also carries a gating attribute " "set to 1 — marked positively, never by the ABSENCE of sev or origin. "; // Emitted only when a clone-family row (duplication / new-clone-of-reused-helper) is in the document, diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh new file mode 100755 index 000000000..94e9a841c --- /dev/null +++ b/test/qddialscheck.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# qddialscheck.sh — the per-kind DIALS of --quality-delta (round 2026-09-10, audit lane Q1's dial table). +# +# Q1 measured the verb on three labelled populations — 12 working-tree replays of landed commits, 40 ref-pair +# replays, and a 15-case synthetic battery — and found the precision problem concentrated in a few kinds while +# the recall headroom sat in others. Each section below is ONE dial, with the case that must stay caught beside +# the case that must stop firing, so a later change cannot quietly restore either half: +# +# 1. short-horizon-churn — churn="self" is informational; GATING needs >= 2 COMMITTED in-window commits on +# the edited lines (the working edit never counted). +# 2. dead-code — the blanket .h/.hpp/.hh/.hxx exclusion is gone; what is exempt is what the LANGUAGE invokes +# (constructors, destructors, operators, bare type declarations, main). +# 3. verbosity/complexity — verbosity counts CODE lines (blank and comment lines are not debt); both kinds +# gate on a bar CROSSING or >= 25% growth, and a sub-bar doubling is a minor row rather than silence. +# 4. api-surface — new-symbol rows are a header COUNT, a surface that SHRANK is not a regression, and a +# single trailing DEFAULTED parameter is minor. +# 5. duplication / new-clone-of-reused-helper — an overload set, a one-file group and a vendored path are +# not this change's duplication. +# 6. error-masking — a block whose only content is a COMMENT is a swallow. +# +# Fixtures are built in temp dirs (git-init where a section needs history); the repo is never touched. +# Usage: RIPWIRE_BIN=build/ripwire bash test/qddialscheck.sh +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" # BOTH seams: positional AND env (a red-first run hands the pre-change binary in) +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +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 " SKIP qddialscheck (git not available)"; exit 0; } + +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT +echo "qddialscheck: BIN=$BIN (temp corpora)" + +# One emitted row, as its own line. The document is newline-free (G4), and a row's own attributes carry +# both '/' (p="path:line") and '"' — so a single-line grep with a character class is the wrong tool and +# silently matched the wrong row when it was tried. Split on '>' first, then match the whole row. +row(){ printf '%s' "$1" | tr '>' '\n' | grep "kind=\"$2\" sym=\"$3\"" ; } +rows(){ printf '%s' "$1" | tr '>' '\n' | grep ' "$CH/src/f.cpp"; } +cm(){ ( cd "$CH" && git add -A >/dev/null 2>&1 && GIT_AUTHOR_DATE="$1" GIT_COMMITTER_DATE="$1" git commit -qm "$2" >/dev/null 2>&1 ); } +OLD="$( date -u -r $(( $( date +%s ) - 200*86400 )) +%Y-%m-%dT%H:%M:%S 2>/dev/null || date -u -d '200 days ago' +%Y-%m-%dT%H:%M:%S )" +NOW="$( date -u +%Y-%m-%dT%H:%M:%S )" +wr 1 2 3 4 ; cm "$OLD" c0 +wr 11 2 33 4 ; cm "$NOW" c1 +wr 11 2 33 44 ; cm "$NOW" c2 +wr 111 222 333 444 +OCH="$( cd "$CH" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +ECH="$( cd "$CH" && "$BIN" . --quality-delta --no-cache >/dev/null 2>&1; echo $? )" +row "$OCH" short-horizon-churn twice | grep -q 'gating="1"' \ + && ok "churn: twice() GATES (two committed in-window rewrites of the edited lines)" \ + || { no "churn: twice() must still gate — the thrash signal was not preserved"; rows "$OCH"; } +row "$OCH" short-horizon-churn once >/dev/null \ + && ok "churn: once() still REPORTED (the kind stays informational, not deleted)" \ + || { no "churn: once() row disappeared — the dial demotes, it does not drop"; rows "$OCH"; } +row "$OCH" short-horizon-churn once | grep -q 'gating="1"' \ + && { no "churn: once() must NOT gate — ONE in-window commit is a touch, not thrash (this is the dial)"; rows "$OCH"; } \ + || ok "churn: once() does not gate (one in-window commit is informational)" +row "$OCH" short-horizon-churn once | grep -q 'sev="minor"' \ + && ok "churn: once() carries sev=minor" \ + || no "churn: once() should be sev=minor" +[ "$ECH" = 2 ] && ok "churn: exit 2 (the gating twice() row fires it)" || no "churn: expected exit 2, got $ECH" +[ "$OCH" = "$( cd "$CH" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "churn: byte-identical run to run (deterministic)" || no "churn: non-deterministic delta" + +[ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" +exit "$fail" diff --git a/test/qualitykindscheck.sh b/test/qualitykindscheck.sh index 4e94899e5..3ecb7d83a 100755 --- a/test/qualitykindscheck.sh +++ b/test/qualitykindscheck.sh @@ -72,10 +72,17 @@ printf '%s' "$OPY" | grep -q 'kind="error-masking" sym="handle"' \ SH="$WORK/shc"; mkdir -p "$SH/src" ( cd "$SH" && git init -q && git config user.email t@t && git config user.name t ) # hot.cpp: two commits (both recent) → will be edited a third time. cold.cpp: one commit → edited once. -printf 'int hot(){ return 1; }\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" +# hot() is MULTI-LINE and gets TWO in-window commits on TWO DIFFERENT lines, because that is what the kind +# gates on since the 2026-09-10 dial round: churn="self" (the edit touches a line committed inside the window) +# is informational, and only "rewritten by >= 2 COMMITTED in-window commits, the working edit not counted" +# fires exit 2. A one-line hot() rewritten by a single commit can never show more than ONE blamed commit, so +# the old fixture could no longer exercise the gating arm at all (see test/qddialscheck.sh §1 for the pair). +printf 'int hot(){\n int x = 1;\n return x;\n}\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" ( cd "$SH" && git add -A >/dev/null 2>&1 && git commit -qm c1 >/dev/null 2>&1 ) -printf 'int hot(){ return 2; }\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" +printf 'int hot(){\n int x = 2;\n return x;\n}\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" ( cd "$SH" && git add -A >/dev/null 2>&1 && git commit -qm c2 >/dev/null 2>&1 ) +printf 'int hot(){\n int x = 2;\n return x + 0;\n}\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" +( cd "$SH" && git add -A >/dev/null 2>&1 && git commit -qm c3 >/dev/null 2>&1 ) # a SEPARATE file with only ONE commit (churn == 1 < 2 → never flags short-horizon-churn even when edited) printf 'int lone(){ return 0; }\nint uselone(){ return lone(); }\n' > "$SH/src/g.cpp" ( cd "$SH" && git add -A >/dev/null 2>&1 && git commit -qm c3single >/dev/null 2>&1 ) @@ -88,10 +95,10 @@ ecsh(){ ( cd "$SH" && "$BIN" . --quality-delta --no-cache >/dev/null 2>&1; echo || { no "short-horizon-churn: clean tree should be clean (exit $( ecsh ))"; dsh | tr '>' '\n' | grep ' "$SH/src/f.cpp" +printf 'int hot(){\n int x = 3;\n return x + 1;\n}\nint cold(){ return 1; }\nint drive(){ return hot()+cold(); }\n' > "$SH/src/f.cpp" printf 'int lone(){ return 9; }\nint uselone(){ return lone(); }\n' > "$SH/src/g.cpp" OSH="$( dsh )" -[ "$( ecsh )" = 2 ] && ok "short-horizon-churn: rewrite of a high-churn file → exit 2" || no "short-horizon-churn: should exit 2 (got $( ecsh ))" +[ "$( ecsh )" = 2 ] && ok "short-horizon-churn: rewrite of lines TWO in-window commits wrote → exit 2" || no "short-horizon-churn: should exit 2 (got $( ecsh ))" printf '%s' "$OSH" | grep -q 'kind="short-horizon-churn" sym="hot"' \ && ok "short-horizon-churn: hot() flagged (file had ≥2 recent commits, rewritten again)" || { no "short-horizon-churn: hot() not flagged"; printf '%s\n' "$OSH" | tr '>' '\n' | grep '/dev/null )" printf '%s' "$OSF" | grep -q 'kind="short-horizon-churn" sym="hot"[^/]*churn="self"' \ && ok "self/ambient churn: in-place edit of a recently-committed line → facet churn=\"self\"" \ || { no "self/ambient churn: expected churn=\"self\" on hot()"; printf '%s\n' "$OSF" | tr '>' '\n' | grep '' '\n' | grep '/dev/null 2>&1; echo $? )" +[ "$ESF" = 0 ] && ok "self/ambient churn: a self-only finding does not gate exit 2" || no "self/ambient churn: self-only run should exit 0 (got $ESF)" # 4b) AMBIENT — adds lines: the working-tree edit only INSERTS a new statement, touching no existing line. AF="$WORK/ambientadd"; mkdir -p "$AF/src" diff --git a/test/regression.sh b/test/regression.sh index 0d54307d8..9fd06f41b 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 35623f5c0f210aeec1350f75eb6bf56beff6451b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:00:13 -0400 Subject: [PATCH 03/73] test(emit): the escaper gate lands before the escaper rewrite it measures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GATE FIRST. The run-copy rewrite of the three emit escapers (rw::escapeXml, rw::appendCdataSafe, rw::jsonesc::escapeInto) is a pure performance change whose whole risk is a byte set that is one member short — output that still LOOKS like text, with a raw '<' where an entity belonged. Nothing in a golden map exercises that: an escaper is only interesting on the bytes a repo does not normally hold. So the comparison arrives first, against the CURRENT per-byte loops, where it passes trivially — and any later divergence is the rewrite's, not the gate's. test/emitescape_harness.cpp freezes the three per-byte loops verbatim as *Ref and asserts byte-identity over 222,682 adversarial inputs: all 256 byte values alone and concatenated; a special byte at EVERY offset of a filler run up to two 32-byte AVX2 blocks (the block-boundary sweep a SIMD run loop plus its scalar tail must survive); overlong 2/3/4-byte forms, UTF-16 surrogate halves, >U+10FFFF, sequences truncated at end-of-buffer, a lone continuation byte as the final byte, a BOM; "]]>" at start/middle/end, "]]]]>", a trailing "]]"; all eight escapeInto flag combinations; and 200k deterministic fuzz strings over an alphabet biased to the special set. It also re-asserts §B12.7's scrub-disclosure predicate against what the escapers actually do, since xmlScrubIsLossy classifies the same byte classes the run loop will skip in bulk. CAN-GO-RED, proved not asserted: the same harness rebuilt with -DEMITESCAPE_MUTATE_BYTESET=1 adds a byteset with '<' DROPPED and requires it to DISAGREE with the reference — 168,423 of 222,682 inputs differ. A comparison blind to a missing set member would report zero and this arm would fail, which is what makes the first arm worth anything. End to end, in a temp dir (never inside the repo): a fixture whose doc-comment carries every byte 0x01..0xFF except '\n' goes through --for (escapeXml, entities + / + the invalid-UTF-8 '?' scrub), --expand (appendCdataSafe, including the ]]> split), and their --json twins (escapeInto). XML piped through `xmllint --noout`; JSON through python3's parser. Each arm first proves the byte soup actually reached the output, so a fixture that silently stopped being ingested cannot pass by emitting nothing. Registered in test/regression.sh's absorb loop; count regenerated by docs/gatecount_build.py (586 -> 587, 8 marked sites). manifestcheck, gatecountcheck, gateexitcheck, shellgateindexcheck, binoverridecheck all green (binoverride sees the new gate among the 556 that go red against the sentinel). --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/emitescape_harness.cpp | 401 +++++++++++++++++++++++++++++++++ test/emitescapecheck.sh | 137 +++++++++++ test/regression.sh | 2 +- 6 files changed, 547 insertions(+), 9 deletions(-) create mode 100644 test/emitescape_harness.cpp create mode 100755 test/emitescapecheck.sh diff --git a/README.md b/README.md index d157ea9f3..b72eb8f4f 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-587 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +588 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 **587 gate scripts** and is the authoritative list; +`test/regression.sh` names **588 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b1058118..6144576de 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` | 587 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 588 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) @@ -5625,7 +5625,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 **587 gate scripts**, all of which exist on disk. +naming **588 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`. @@ -6637,7 +6637,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 587. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 588. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index fc5a0220f..606d106f2 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["587 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 + ["588 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "587", "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, "588", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["588 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/test/emitescape_harness.cpp b/test/emitescape_harness.cpp new file mode 100644 index 000000000..b9427a216 --- /dev/null +++ b/test/emitescape_harness.cpp @@ -0,0 +1,401 @@ +// emitescape_harness.cpp — byte-identity harness for the RUN-COPY rewrite of the three emit escapers +// (rw::escapeXml and rw::appendCdataSafe in src/serialize.h, rw::jsonesc::escapeInto in +// src/infra/jsonesc.h). All three are header-only, so this calls them directly rather than diffing a +// whole map, and it is independent of the ripwire binary and of main.cpp. +// +// THE CONTRACT UNDER TEST. The rewrite replaces a per-byte switch with "find the next byte that is IN +// the special set (strkern::findByteset), copy the clean run in one memcpy, handle that one byte with +// the SAME switch, repeat". That is a pure performance change: the emitted bytes must not move, on ANY +// input, including the ones a hand-written byte set is most likely to get wrong. So this harness keeps +// the ORIGINAL per-byte loops verbatim as `*Ref` below and asserts the shipped function agrees with +// them byte-for-byte. The references are frozen copies — if the shipped semantics ever legitimately +// change, the reference changes in the same commit and the gate says so out loud. +// +// Cases proved: +// A every one of the 256 byte values, alone and concatenated in order. +// B a special byte planted at EVERY offset of a 0..96-byte filler string — the block-boundary sweep +// that a 16-byte NEON / 32-byte AVX2 run loop plus its scalar tail must survive. +// C invalid UTF-8: bare continuation, overlong 2/3/4-byte forms, UTF-16 surrogate halves, >U+10FFFF, +// a sequence TRUNCATED at end-of-buffer, and a lone continuation byte as the final byte. +// D valid multibyte (Latin-1 range, CJK, astral) and a UTF-8 BOM, alone and around specials. +// E CDATA: "]]>" at the start, mid, and end of a body, "]]]]>", and a trailing "]]". +// F all four (escapeAngleAmp, validateUtf8) combinations of escapeInto, plus both +// replacementAsTextEscape postures. +// G 200k deterministic fuzz strings over an alphabet biased to the special set. +// MUT a can-go-red arm: a byteset with '<' DROPPED (compiled in with -DEMITESCAPE_MUTATE_BYTESET=1 +// as `escapeXmlMutatedSet`) MUST disagree with the reference. If it agrees, the comparison is +// not looking at what it claims to and the gate is worthless. +// +// Exit 0 = all pass; nonzero = a failure. + +#include "../src/serialize.h" +#include "../src/infra/jsonesc.h" + +#include +#include +#include +#include + +using namespace rw; + +static int g_fail = 0; +static int g_checks = 0; + +static void check( bool cond, const char* msg ) +{ + ++g_checks; + if( !cond ) + { + std::printf( " FAIL %s\n", msg ); + g_fail = 1; + } +} + +// ── the frozen per-byte references (verbatim copies of the pre-rewrite loops) ────────────────────────── + +static std::string escapeXmlRef( std::string_view s ) +{ + std::string out; + const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; + const char* d = s.data(); + const std::size_t n = s.size(); + for( std::size_t i = 0; i < n; ) + { + const char c = d[i]; + switch( c ) + { + case '&': put( "&" ); ++i; break; + case '<': put( "<" ); ++i; break; + case '>': put( ">" ); ++i; break; + case '"': put( """ ); ++i; break; + case '\'': put( "'" ); ++i; break; + case '\t': + case '\n': + case '\r': put( xmlControlCharRef( c ) ); ++i; break; + default: + if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } + else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } + else + { + for( int k = 0; k < len; ++k ) + { + out.push_back( d[i + k] ); + } + i += std::size_t( len ); + } + } + } + return out; +} + +static std::string appendCdataSafeRef( std::string_view body ) +{ + std::string safe; + const char* d = body.data(); + const std::size_t n = body.size(); + for( std::size_t i = 0; i < n; ) + { + if( i + 2 < n && d[i] == ']' && d[i + 1] == ']' && d[i + 2] == '>' ) + { safe += "]]]]>"; i += 3; continue; } + const unsigned char c = static_cast( d[i] ); + if( c < 0x80 ) { safe += xmlSafeByte( d[i] ); ++i; } + else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { safe += '?'; ++i; } + else { safe.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } + } + return safe; +} + +static std::string escapeIntoRef( std::string_view s, bool escapeAngleAmp, bool validateUtf8, bool replacementAsTextEscape ) +{ + std::string out; + const char* d = s.data(); + const std::size_t n = s.size(); + std::size_t i = 0; + while( i < n ) + { + const unsigned char c = static_cast( d[i] ); + if( c < 0x80 ) + { + switch( c ) + { + case '"': out += "\\\""; ++i; continue; + case '\\': out += "\\\\"; ++i; continue; + case '\n': out += "\\n"; ++i; continue; + case '\r': out += "\\r"; ++i; continue; + case '\t': out += "\\t"; ++i; continue; + case '<': if( escapeAngleAmp ) { out += "\\u003c"; ++i; continue; } break; + case '>': if( escapeAngleAmp ) { out += "\\u003e"; ++i; continue; } break; + case '&': if( escapeAngleAmp ) { out += "\\u0026"; ++i; continue; } break; + default: break; + } + if( c < 0x20 ) + { char b[ 8 ]; std::snprintf( b, sizeof( b ), "\\u%04x", unsigned( c ) ); out += b; } + else + { + out += char( c ); + } + ++i; + continue; + } + if( !validateUtf8 ) { out += char( c ); ++i; continue; } + const int len = jsonesc::utf8SeqLen( d, i, n ); + if( len == 0 ) + { + if( replacementAsTextEscape ) { out += "\\ufffd"; } + else { out += "\xEF\xBF\xBD"; } + ++i; + } + else { out.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } + } + return out; +} + +// ── the shipped functions, wrapped to the same signature ────────────────────────────────────────────── + +static std::string escapeXmlNew( std::string_view s ) +{ + std::vector buf; + const std::string_view v = escapeXml( s, buf ); + return std::string( v ); +} + +static std::string appendCdataSafeNew( std::string_view s ) +{ + std::string out; + appendCdataSafe( s, out ); + return out; +} + +static std::string escapeIntoNew( std::string_view s, bool a, bool v, bool r ) +{ + std::string out; + jsonesc::escapeInto( s, out, a, v, r ); + return out; +} + +// ── MUT: the same run-copy shape with '<' dropped from the byte set ─────────────────────────────────── +// Deliberately WRONG. Not compiled into anything shipped; it exists so the harness can prove that its +// comparison actually notices a set member going missing (a byteset bug is silent otherwise — the +// output is still well-formed-looking text, just with a raw '<' where an entity belonged). +#if EMITESCAPE_MUTATE_BYTESET +static std::string escapeXmlMutatedSet( std::string_view s ) +{ + std::string out; + const char* d = s.data(); + const std::size_t n = s.size(); + const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; + for( std::size_t i = 0; i < n; ) + { + const char c = d[i]; + switch( c ) + { + // '<' intentionally absent from the set — falls through to the verbatim copy below. + case '&': put( "&" ); ++i; break; + case '>': put( ">" ); ++i; break; + case '"': put( """ ); ++i; break; + case '\'': put( "'" ); ++i; break; + case '\t': + case '\n': + case '\r': put( xmlControlCharRef( c ) ); ++i; break; + default: + if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } + else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } + else + { + for( int k = 0; k < len; ++k ) { out.push_back( d[i + k] ); } + i += std::size_t( len ); + } + } + } + return out; +} +#endif + +// ── the corpus ──────────────────────────────────────────────────────────────────────────────────────── + +// UB-free deterministic generator (same shape as test/harnesscommon.h's, kept local so this TU needs +// no extra include path). +struct Rng +{ + std::uint64_t state = 0x9E3779B97F4A7C15ull; + std::uint64_t next() noexcept + { + state = state * 6364136223846793005ull ^ 1442695040888963407ull; + std::uint64_t m = state; + m ^= m >> 33; m *= 0xFF51AFD7ED558CCDull; m ^= m >> 33; + return m; + } +}; + +static void addCase( std::vector& v, std::string s ) { v.push_back( std::move( s ) ); } + +static std::vector buildCorpus() +{ + std::vector cases; + + // A — every byte value alone, and all 256 in order. + std::string all; + for( int b = 0; b < 256; ++b ) + { + addCase( cases, std::string( 1, char( b ) ) ); + all.push_back( char( b ) ); + } + addCase( cases, all ); + addCase( cases, std::string() ); + + // B — a special byte planted at every offset of a filler run, across every length up to two + // 32-byte AVX2 blocks plus a tail. + const char specials[] = { '&', '<', '>', '"', '\'', '\t', '\n', '\r', '\0', '\x0b', '\x1f', '\x7f', + char( 0x80 ), char( 0xC3 ), char( 0xFF ), ']' }; + for( char sp : specials ) + { + for( std::size_t len = 1; len <= 96; ++len ) + { + for( std::size_t at = 0; at < len; at += ( len > 40 ? 7 : 1 ) ) + { + std::string s( len, 'a' ); + s[at] = sp; + addCase( cases, s ); + } + } + } + + // C — invalid UTF-8 shapes. + const char* bad[] = { + "\x80", "\xBF", "\xC0\x80", "\xC1\xBF", "\xC2", "\xE0\x80\x80", "\xE0\x9F\xBF", + "\xED\xA0\x80", "\xED\xBF\xBF", "\xE2\x82", "\xF0\x80\x80\x80", "\xF0\x8F\xBF\xBF", + "\xF4\x90\x80\x80", "\xF5\x80\x80\x80", "\xFE", "\xFF", "\xF0\x9D\x84", + }; + for( const char* b : bad ) + { + std::string s( b ); + addCase( cases, s ); + addCase( cases, "abc" + s ); + addCase( cases, s + "abc" ); + addCase( cases, "abc" + s + "<&>" ); + addCase( cases, std::string( 31, 'x' ) + s ); + addCase( cases, std::string( 32, 'x' ) + s ); + addCase( cases, std::string( 33, 'x' ) + s ); + } + // lone continuation byte as the very last byte of the buffer + addCase( cases, std::string( 40, 'q' ) + "\xBF" ); + addCase( cases, std::string( 40, 'q' ) + "\xE2\x82" ); + + // D — valid multibyte + BOM. + const char* good[] = { "\xC3\xA9", "\xE2\x82\xAC", "\xF0\x9D\x84\x9E", "\xEF\xBB\xBF", "\xEF\xBF\xBD" }; + for( const char* g : good ) + { + std::string s( g ); + addCase( cases, s ); + addCase( cases, s + "<" + s ); + addCase( cases, std::string( 30, 'z' ) + s + std::string( 30, 'z' ) ); + addCase( cases, std::string( 31, 'z' ) + s ); + } + + // E — CDATA close sequences. + addCase( cases, "]]>" ); + addCase( cases, "]]" ); + addCase( cases, "]" ); + addCase( cases, "]]]" ); + addCase( cases, "]]]]>" ); + addCase( cases, "a]]>b" ); + addCase( cases, "]]>]]>" ); + addCase( cases, std::string( 31, 'p' ) + "]]>" ); + addCase( cases, std::string( 32, 'p' ) + "]]>" + std::string( 32, 'p' ) ); + addCase( cases, std::string( 30, 'p' ) + "]]" ); + addCase( cases, "]]\x01>" ); + + // G — deterministic fuzz over an alphabet biased to the special set. + Rng rng; + const std::string alphabet = "abcdefgh<>&\"'\t\n\r]] \x01\x1f\x7f\x80\xC3\xA9\xE2\x82\xAC\xF0\x9D\x84\x9E\xFF"; + for( int k = 0; k < 200000; ++k ) + { + const std::size_t len = std::size_t( rng.next() % 201 ); + std::string s; + s.reserve( len ); + for( std::size_t j = 0; j < len; ++j ) + { + s.push_back( alphabet[ std::size_t( rng.next() % alphabet.size() ) ] ); + } + cases.push_back( std::move( s ) ); + } + return cases; +} + +int main() +{ + const std::vector cases = buildCorpus(); + std::printf( "emitescape_harness: %zu inputs\n", cases.size() ); + + std::size_t xmlBad = 0, cdataBad = 0, jsonBad = 0; + for( const std::string& s : cases ) + { + if( escapeXmlNew( s ) != escapeXmlRef( s ) ) + { + if( xmlBad == 0 ) { std::printf( " first escapeXml mismatch, len=%zu\n", s.size() ); } + ++xmlBad; + } + if( appendCdataSafeNew( s ) != appendCdataSafeRef( s ) ) + { + if( cdataBad == 0 ) { std::printf( " first appendCdataSafe mismatch, len=%zu\n", s.size() ); } + ++cdataBad; + } + for( int mode = 0; mode < 8; ++mode ) + { + const bool a = ( mode & 1 ) != 0; + const bool v = ( mode & 2 ) != 0; + const bool r = ( mode & 4 ) != 0; + if( escapeIntoNew( s, a, v, r ) != escapeIntoRef( s, a, v, r ) ) + { + if( jsonBad == 0 ) { std::printf( " first escapeInto mismatch, mode=%d len=%zu\n", mode, s.size() ); } + ++jsonBad; + } + } + } + check( xmlBad == 0, "escapeXml byte-identical to the frozen per-byte reference" ); + check( cdataBad == 0, "appendCdataSafe byte-identical to the frozen per-byte reference" ); + check( jsonBad == 0, "escapeInto byte-identical to the frozen per-byte reference (8 flag combos)" ); + if( xmlBad ) { std::printf( " escapeXml mismatches: %zu\n", xmlBad ); } + if( cdataBad ) { std::printf( " appendCdataSafe mismatches: %zu\n", cdataBad ); } + if( jsonBad ) { std::printf( " escapeInto mismatches: %zu\n", jsonBad ); } + + // scrub-disclosure predicate must keep agreeing with what the escapers actually DO (§B12.7): the + // lossy-tell is derived from the same byte classes the run loop now skips over in bulk. + std::size_t lossyBad = 0; + for( const std::string& s : cases ) + { + const bool lossy = xmlScrubIsLossy( s ); + const bool cdataHit = appendCdataSafeRef( s ) != std::string( s ) && true; + (void)cdataHit; + // a lossy input is exactly one whose escaped form contains '?' or a substituted space that the + // input did not have; assert the cheap direction: not-lossy ⇒ no '?' introduced. + if( !lossy ) + { + std::string ref = appendCdataSafeRef( s ); + std::string plain( s ); + // appendCdataSafe only splits ]]> on non-lossy input; strip that expansion before comparing + std::string expanded; + for( std::size_t i = 0; i < plain.size(); ) + { + if( i + 2 < plain.size() && plain[i] == ']' && plain[i + 1] == ']' && plain[i + 2] == '>' ) + { expanded += "]]]]>"; i += 3; } + else { expanded += plain[i]; ++i; } + } + if( ref != expanded ) { ++lossyBad; } + } + } + check( lossyBad == 0, "xmlScrubIsLossy(false) really means appendCdataSafe moved no byte" ); + +#if EMITESCAPE_MUTATE_BYTESET + std::size_t mutDiff = 0; + for( const std::string& s : cases ) + { + if( escapeXmlMutatedSet( s ) != escapeXmlRef( s ) ) { ++mutDiff; } + } + check( mutDiff > 0, "MUT: a byteset missing '<' DISAGREES with the reference (the gate can go red)" ); + std::printf( " MUT: %zu of %zu inputs differ\n", mutDiff, cases.size() ); +#endif + + std::printf( "emitescape_harness: %d checks, %s\n", g_checks, g_fail ? "FAIL" : "ALL PASS" ); + return g_fail; +} diff --git a/test/emitescapecheck.sh b/test/emitescapecheck.sh new file mode 100755 index 000000000..f1bc10c2e --- /dev/null +++ b/test/emitescapecheck.sh @@ -0,0 +1,137 @@ +#!/usr/bin/env bash +# emitescapecheck.sh — gate for the RUN-COPY rewrite of the three emit escapers: rw::escapeXml and +# rw::appendCdataSafe (src/serialize.h) and rw::jsonesc::escapeInto (src/infra/jsonesc.h). +# +# WHY A HARNESS AND NOT A GOLDEN DIFF. The rewrite is "find the next byte in the special set with +# strkern::findByteset, memcpy the clean run, handle that one byte with the SAME switch". Nothing in a +# golden map exercises the inputs that shape gets wrong — an escaper is only interesting on the bytes a +# repo does not normally contain. So the harness (test/emitescape_harness.cpp) keeps the ORIGINAL +# per-byte loops verbatim as `*Ref` and asserts byte-identity over an adversarial corpus: every one of +# the 256 byte values; a special byte at EVERY offset of a filler run up to two 32-byte AVX2 blocks +# (the block-boundary sweep a SIMD run loop plus its scalar tail must survive); overlongs, surrogate +# halves, >U+10FFFF, truncated sequences, a lone continuation byte as the final byte of the buffer, a +# BOM; "]]>" at the start/middle/end and "]]]]>"; all eight escapeInto flag combinations; and 200k +# deterministic fuzz strings over an alphabet biased to the special set. +# +# ARMS +# (A) harness compiles and passes — the shipped escapers agree with the frozen references. +# (B) CAN-GO-RED: the same harness recompiled with -DEMITESCAPE_MUTATE_BYTESET=1, which adds a +# byteset with '<' DROPPED. That build asserts the mutant DISAGREES with the reference. A +# comparison that could not see a missing set member would report zero differences and this arm +# would fail — which is the point: it proves arm (A) is looking at what it claims to. +# (C) END TO END: a fixture tree (in a temp dir, NEVER inside the repo — see the +# "gate fixture is the live repo" trap) whose doc-comment carries every byte value 0x01..0xFF +# except '\n'. The map of that tree must pipe clean through `xmllint --noout` (G4), and the +# --json map of the same tree must be accepted by python3's json parser. Both surfaces are the +# ones the rewritten escapers write, so a set bug that produced a raw '<' or a broken UTF-8 +# sequence turns this red without any reference to compare against. +# +# Usage: bash test/emitescapecheck.sh (binary from RIPWIRE_BIN, else ./build/ripwire) +# CXX=clang++ bash test/emitescapecheck.sh +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +CXX="${CXX:-c++}" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +. "$ROOT/scripts/cxxstd.sh" +CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" +HARNESS="$ROOT/test/emitescape_harness.cpp" +WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT + +echo "emitescapecheck: CXX=$CXX BIN=$BIN" + +compile_arm() # $1=output $2...=extra flags +{ + local out="$1"; shift + "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra "$@" \ + -I"$ROOT/src/infra" -I"$ROOT/third_party" -I"$ROOT/src" \ + "$HARNESS" "$ROOT/src/infra/diagnostics.cpp" -o "$out" 2> "$WORK/cc.log" +} + +# ── (A) the shipped escapers vs the frozen per-byte references ──────────────────────────────────────── +if compile_arm "$WORK/plain"; then + if "$WORK/plain" > "$WORK/plain.out" 2>&1; then + ok "escapers byte-identical to the frozen per-byte references over the adversarial corpus" + sed -n 's/^/ /p' "$WORK/plain.out" | head -4 + else + no "harness reported a mismatch"; sed 's/^/ /' "$WORK/plain.out" | head -20 + fi +else + no "harness failed to compile"; sed 's/^/ /' "$WORK/cc.log" | head -20 +fi + +# ── (B) can-go-red: a byteset with '<' dropped must be VISIBLE to the comparison ─────────────────────── +if compile_arm "$WORK/mut" -DEMITESCAPE_MUTATE_BYTESET=1; then + if "$WORK/mut" > "$WORK/mut.out" 2>&1; then + ok "MUT arm: a byteset missing '<' is detected (the comparison can go red)" + grep -n 'MUT:' "$WORK/mut.out" | sed 's/^/ /' + else + no "MUT arm did not detect a byteset missing '<' — arm (A) proves nothing" + sed 's/^/ /' "$WORK/mut.out" | head -20 + fi +else + no "MUT arm failed to compile"; sed 's/^/ /' "$WORK/cc.log" | head -20 +fi + +# ── (C) end to end: every byte value through a real map, XML and JSON ───────────────────────────────── +[ -x "$BIN" ] || { no "binary not found: $BIN"; echo "emitescapecheck: FAIL"; exit 2; } + +FIX="$WORK/fixture" +mkdir -p "$FIX" +python3 - "$FIX" <<'PY' +import os, sys +d = sys.argv[1] +# every byte 0x01..0xFF except '\n' (0x0A), which would end the line comment, on ONE doc-comment line; +# 0x00 is left out on purpose — an ingest that classifies a NUL-bearing file as binary would skip the +# file and the arm would prove nothing. NUL's escape path is covered by the harness instead. +soup = bytes( b for b in range( 1, 256 ) if b != 0x0A ) +body = b"// bytesoup: " + soup + b"\n// ]]> and \" ' inside a comment\n" \ + b"void fixtureAlpha( int n ) { (void)n; }\n" \ + b"/** every byte again in a block comment: " + soup + b" */\n" \ + b"int fixtureBeta( int n ) { return fixtureAlpha2( n ); }\n" \ + b"int fixtureAlpha2( int n ) { return n; }\n" +open( os.path.join( d, "soup.cpp" ), "wb" ).write( body ) +open( os.path.join( d, "plain.cpp" ), "wb" ).write( b"int plainOne( int n ) { return n + 1; }\n" ) +PY + +# The FLAGLESS map carries no doc-comment and no body, so it would prove nothing about the escapers. +# --for puts the doc-comment through escapeXml (entities + / + the invalid-UTF-8 '?' scrub) and +# --expand puts the whole file through appendCdataSafe (including the "]]>" split); their --json twins +# put the same bytes through jsonesc::escapeInto. All four surfaces are checked. +run_arm() # $1=label $2=validator(xml|json) $3...=ripwire args +{ + local label="$1" kind="$2"; shift 2 + if ! "$BIN" "$FIX" "$@" > "$WORK/out.$kind" 2>"$WORK/err.$kind"; then + no "ripwire failed on the every-byte fixture ($label)"; sed 's/^/ /' "$WORK/err.$kind" | head -10; return + fi + if ! LC_ALL=C grep -q 'bytesoup' "$WORK/out.$kind"; then + no "$label: the byte soup never reached the output — this arm proves nothing"; return + fi + if [ "$kind" = xml ]; then + if xmllint --noout "$WORK/out.$kind" 2>"$WORK/xmllint.err"; then + ok "$label: well-formed XML over every byte value (G4)" + else + no "$label: xmllint rejected the map"; sed 's/^/ /' "$WORK/xmllint.err" | head -10 + fi + else + if python3 -c 'import json,sys; json.load(open(sys.argv[1],encoding="utf-8"))' "$WORK/out.$kind"; then + ok "$label: parses as JSON (valid UTF-8, valid escapes) over every byte value" + else + no "$label: output is not parseable JSON" + fi + fi +} + +run_arm "--for (escapeXml)" xml --for="bytesoup fixture" +run_arm "--expand (appendCdataSafe)" xml --expand=fixtureAlpha +run_arm "--for --json (escapeInto)" json --for="bytesoup fixture" --json +run_arm "--pack-task --json (bodies)" json --pack-task="bytesoup fixture" --json + +if [ "$fail" -eq 0 ]; then + echo "emitescapecheck: ALL PASS"; exit 0 +else + echo "emitescapecheck: FAIL"; exit 2 +fi diff --git a/test/regression.sh b/test/regression.sh index 290158b3e..ee8683e2e 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck strkerncheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 209f97a43bf17b2a4cb3c43a74e8435362972ac8 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:09:44 -0400 Subject: [PATCH 04/73] fix(capsweep): the harness could not tell a refusal from an answer, and its own output was in the corpus MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Six defects in one instrument, every one of them the shape the instrument was built to prevent. Arms A-F of test/capsweepcheck.sh are source-level and NEVER execute a corpus row, which is exactly how all six shipped inside the phase they were meant to guard. 1. shlex.split RAISES ValueError on the two corpus rows carrying an unbalanced quote, and run_corpus caught only TimeoutExpired — so the fix that rescued 37 mangled rows turned `screen` into a hard crash. Now recorded UNPARSEABLE. The handler is spelled `as parseErr`: the child environment two lines above is named `e`, Python DELETES an except-name at block end, and `except ValueError as e` kills the NEXT row with UnboundLocalError. 2. run_corpus recorded len(stdout) and discarded returncode, so a REFUSAL (exit 1, no output) and an ANSWER OF NOTHING were the same measurement. 57 zero-byte baselines sat in the record with no way to classify them. Every row now carries a state (ok / rc=N / unparseable / unexpanded / timeout) and only `ok` carries a byte count. 3. A run in which NOTHING answered printed `cap-sensitive: 0 (0%)` and exited 0. It now refuses to report a split or write records. 4. The denominator counted 56 rows that emit nothing at all. It is now the ANSWERING rows, and the TSV records carry the recipe: a row that emits nothing cannot respond to a cap. 5. os.path.expandvars reads os.environ, not the dict run_corpus builds. With RIPWIRE_CAPSWEEP_TMP unset in the operator's shell — the normal case — nine rows ran with the LITERAL string, and --cache=/--export=/--html= wrote it as a relative path INSIDE the frozen corpus: a 10.4 MB cache blob that --batch= then read back, "responding" to 103 of 108 caps. Expansion now reads the child env, an UNDEFINED variable is refused rather than passed through, and the destination must resolve outside the corpus. 6. `git archive HEAD` leaves no .git, so every git verb measured its degraded path — while the RECORDED run measured a foreign repository, because ripwire walks up for .git in its own code (src/gitmine.h:2792, src/ingest_crawl.h:929): the record has --stray-content=lane/ --plan at 11,670,369 B on a corpus with no branches at all. Two guards, because one is not enough: assert_corpus_clean now refuses a corpus with a git repo in any STRICT ancestor (ripwire honours no ceiling variable), and run_corpus sets GIT_CEILING_DIRECTORIES for the git processes ripwire spawns. The corpus then gets its OWN tiny history — three commits over the same four markdown files plus a dirty working tree — so the git verbs measure their real path: verb git archive HEAD + 3-commit fixture . --handoff 2,476 B 3,918 B (changed=0 -> real rows) . --cochange 0 B (rc=1) 2,937 B . --situ 0 B (rc=1) 1,705 B . --pr-context 150 B 7,947 B . --quality-delta 0 B (rc=1) 7,255 B . --merge-scout=HEAD~2,HEAD~1 0 B (rc=1) 3,299 B . --dmm 0 B (rc=1) 3,045 B The fixture touches four markdown files with a comment line, so it adds no symbol to the map. It does NOT exercise per-file symbol caps — one appended line is one changed symbol — and the docstring says so rather than letting a silence be read as evidence. Plus the guard for the class rather than the instance: a file-list fingerprint of the corpus taken after freeze_corpus and re-checked after EVERY arm, aborting with the path. assert_corpus_clean guards one hardcoded directory name; defect 5 arrived through a name it could never have known. GATE. New arms G-M in test/capsweepcheck.sh drive the production screen_core through two new phases (`run-corpus` against a stub binary over a six-row synthetic corpus, `plant-history` against a synthetic tree) — still no build. Each was proven red by mutating the fix it covers: mutation arm that reds no ValueError handler (G) run-corpus failed on the synthetic corpus refusal recorded as len(stdout) (H) the refusing row was not recorded as a distinct state no green-while-inert refusal (I) control: the inert run failed for the wrong reason denominator is every row (I) the split was not reported over the answering rows os.path.expandvars (G) a row wrote into the corpus and the fingerprint fired no fingerprint re-check (both sites) (K) a row that wrote a file into the corpus was measured anyway no ancestor scan (L) a corpus with a git repository ABOVE it was ACCEPTED fixture files absent (M) control: an empty history was planted silently docs/TUNING.md is NOT regenerated here: the census and the values must settle first, and `emit` refuses a sweep whose measured cap value no longer matches src/. The re-run lands at the end of this branch, with the split before and after. --- bench/capsweep/capsweep.py | 447 +++++++++++++++++++++++++++++++++---- test/capsweepcheck.sh | 216 ++++++++++++++++++ 2 files changed, 616 insertions(+), 47 deletions(-) diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index 36b9bffb6..f9adaed28 100644 --- a/bench/capsweep/capsweep.py +++ b/bench/capsweep/capsweep.py @@ -25,12 +25,29 @@ patch run the cap patcher against --root SOMETREE and stop. The gate hands it a synthetic tree and reads the rewritten line. check-corpus run the corpus-cleanliness assertion against --corpus SOMEDIR and stop. The gate hands - it a synthetic corpus with bench/capsweep/ inside and asserts the refusal. + it a synthetic corpus with bench/capsweep/ inside and asserts the refusal, and one with + a .git ABOVE it and asserts that refusal too. + plant-history plant the synthetic git history in --root and stop. `git archive HEAD` leaves no + .git at all, so without it ~20 git-dependent corpus rows measure their DEGRADED path + (--handoff reports changed="0"; --cochange/--situ/--map-diff exit 1 with 0 bytes) and + are scored "responds to NO cap" while measuring a refusal. + run-corpus run both screen arms against --binary (a stub) over --corpus-file and stop. The gate + hands it six synthetic rows — one answering, one cap-sensitive, one refusing, one with an + unbalanced quote, one naming an undefined variable, one that litters the corpus — and + reads the census, the refusal and the denominator back out. + +EVERY ROW CARRIES A STATE, and only `ok` carries a byte count. `len(stdout)` alone made a REFUSAL +(exit 1, no output) and an ANSWER OF NOTHING the same measurement, which is how 37 corpus rows mangled +by a quoting bug sat inside "responds to NO cap" for a whole round: 0 under both arms is a delta of 0, +and a delta of 0 leaves the sensitive set silently. The split's denominator is the rows that ANSWERED — +a row that emits nothing cannot respond to a cap — and a run in which NO row answered refuses to report +a split at all rather than printing a clean 0%. Usage: python3 bench/capsweep/capsweep.py prepare|screen|sweep|emit [--scratch DIR] [--jobs N] python3 bench/capsweep/capsweep.py emit [--data bench/capsweep] [--out docs/TUNING.md] [--check] python3 bench/capsweep/capsweep.py patch --root TREE python3 bench/capsweep/capsweep.py check-corpus --corpus DIR + python3 bench/capsweep/capsweep.py run-corpus --binary B --corpus DIR --corpus-file F [--bump K=V] WHERE THE MEASUREMENTS LIVE, AND WHY THEY ARE TSV. `prepare`/`screen`/`sweep` write their records into --scratch, never into the repo and never into the frozen corpus. Publishing a round means copying @@ -126,24 +143,92 @@ def read_corpus(): return [l for l in (HERE / 'corpus.txt').read_text().splitlines() if l.strip() and not l.lstrip().startswith('#')] +# ── what a corpus row DID, which is not the same question as how many bytes it produced ───────────── +# The harness recorded `len(stdout)` and nothing else, so a row that REFUSED (exit 1, no output) and a +# row that ANSWERED with nothing were the same measurement: 0. That is how 37 mangled rows sat inside +# "responds to NO cap" for a whole round — a row measuring 0 under both arms has a delta of 0 and leaves +# the sensitive set silently. Every row now carries a STATE, and only kStateOk carries a byte count; the +# other three record kNullField, because a refusal is not a measurement of zero bytes. +kStateOk = 'ok' # exit 0 — the byte count means something +kStateTimeout = 'timeout' # neither arm's value is known +kStateUnparseable = 'unparseable' # shlex could not split the row: a corpus defect, not a result +kStateUnexpanded = 'unexpanded' # the row names a variable this harness does not define (see F17) + +def state_refused( rc ): + return 'rc=%d' % rc + +def answered( sizes, states, line ): + """The one definition of "this row produced an answer", used by every count below.""" + return states.get( line ) == kStateOk and ( sizes.get( line ) or 0 ) > 0 + +VAR = re.compile(r'\$(\w+)|\$\{(\w+)\}') + +def expandvars_from(word, env): + """Expand $VARS from the environment the CHILD will get — not from os.environ. + + os.path.expandvars reads os.environ, and the harness binds RIPWIRE_CAPSWEEP_TMP in a dict it hands + subprocess.run. With the variable unset in the operator's shell — the normal case, and the one the + corpus comment was written for — all nine rows naming it received the LITERAL string + `$RIPWIRE_CAPSWEEP_TMP`, and `--cache=`/`--export=`/`--html=` then wrote it as a relative path INSIDE + the frozen corpus (a 10.4 MB cache blob). `--batch=$RIPWIRE_CAPSWEEP_TMP` read that blob back and + "responded" to 103 of 108 caps: its input was the accumulated output of the run measuring it. + + An unresolved variable RAISES rather than passing through as a literal. os.path.expandvars leaves it + alone, which is the shell's rule and exactly the behaviour that turned a variable into a path. + """ + missing = [] + def one(m): + name = m.group(1) or m.group(2) + if name not in env: + missing.append(name) + return m.group(0) + return env[name] + out = VAR.sub(one, word) + if missing: + raise KeyError(', '.join(sorted(set(missing)))) + return out + +def assert_tmp_outside(tmp, corpus): + """The scratch path corpus rows write into must not resolve INSIDE the corpus. + + `--cache=`, `--export=`, `--html=` and `--brief=` take a destination, and run_corpus runs with + cwd=corpus. A destination that lands in the corpus makes the harness write into the tree it is + measuring — the artifact assert_corpus_clean exists past, arriving through a path that assertion + does not check. + """ + t, c = pathlib.Path(tmp).resolve(), pathlib.Path(corpus).resolve() + if t == c or c in t.parents: + sys.exit('capsweep: RIPWIRE_CAPSWEEP_TMP (%s) resolves INSIDE the corpus (%s) — corpus rows would\n' + ' write into the tree being measured' % (t, c)) + return str(t) + def run_corpus(binary, root, corpus, env, timeout=120): - """Run each corpus line and return its stdout SIZE. + """Run each corpus line; return ({line: stdout size or None}, {line: state}). Two things here were learned the hard way: - the corpus lines already carry their own root ("." where the verb takes one), so this must NOT prepend one; doing so passed the root twice and every command refused; - stdin is DEVNULL, because `--from-trace=-` reads stdin and otherwise blocks until the timeout. - $VARS in a corpus line are expanded from the environment. The corpus was harvested from a recorded - showcase run whose scratch directory was a machine-local macOS temp path; committing that literal - would have pinned the corpus to one laptop and put somebody's filesystem layout in a public file, so - those occurrences are spelled $RIPWIRE_CAPSWEEP_TMP and bound here. + $VARS in a corpus line are expanded from the environment THIS FUNCTION BUILDS (expandvars_from — not + os.path.expandvars). The corpus was harvested from a recorded showcase run whose scratch directory + was a machine-local macOS temp path; committing that literal would have pinned the corpus to one + laptop and put somebody's filesystem layout in a public file, so those occurrences are spelled + $RIPWIRE_CAPSWEEP_TMP and bound here. + + GIT_CEILING_DIRECTORIES stops the `git` processes ripwire spawns from walking out of the corpus. + It is NOT the whole guard: ripwire walks up for `.git` in its own code (src/gitmine.h, + src/ingest_crawl.h) and honours no such variable, so assert_corpus_clean's ancestor scan is what + actually keeps a git verb from measuring the operator's repository. """ e = dict(os.environ) - e.setdefault('RIPWIRE_CAPSWEEP_TMP', str(pathlib.Path(root).parent / 'corpus-tmp')) + if not e.get('RIPWIRE_CAPSWEEP_TMP'): # not setdefault: an EMPTY value is not a binding, and + e['RIPWIRE_CAPSWEEP_TMP'] = str(pathlib.Path(root).parent / 'corpus-tmp') # Path('') is the CWD e.update(env) + e['RIPWIRE_CAPSWEEP_TMP'] = assert_tmp_outside(e['RIPWIRE_CAPSWEEP_TMP'], root) + e['GIT_CEILING_DIRECTORIES'] = str(pathlib.Path(root).resolve().parent) os.makedirs(e['RIPWIRE_CAPSWEEP_TMP'], exist_ok=True) - out = {} + sizes, states = {}, {} for line in corpus: try: # shlex, not line.split(): 37 of the 195 corpus rows carry a quoted multi-word value @@ -153,13 +238,32 @@ def run_corpus(binary, root, corpus, env, timeout=120): # A row that measures 0 both sides has a delta of 0 and silently leaves the cap-sensitive # set, so those 19% were not measuring the caps they were written to exercise. Verified # against the real corpus: shlex.split gives exit 0 / 2530 B where split() gives exit 1 / 0 B. - argv = [os.path.expandvars(w) for w in shlex.split(line)] + # + # And shlex RAISES on an unbalanced quote, which two corpus rows carry. Catching only + # TimeoutExpired turned the fix into a hard crash of the whole phase. NOT `as e`: the child + # environment two lines above is named `e`, Python DELETES an except-name at block end, and + # the obvious one-line repair therefore kills the NEXT row with UnboundLocalError. + argv = [expandvars_from(w, e) for w in shlex.split(line)] + except ValueError as parseErr: + sizes[line], states[line] = None, '%s: %s' % (kStateUnparseable, parseErr) + continue + except KeyError as missingVar: + sizes[line], states[line] = None, '%s: $%s' % (kStateUnexpanded, missingVar.args[0]) + continue + try: r = subprocess.run([str(binary)] + argv + ['--no-cache'], cwd=str(root), capture_output=True, stdin=subprocess.DEVNULL, env=e, timeout=timeout) - out[line] = len(r.stdout) except subprocess.TimeoutExpired: - out[line] = None # recorded, never silently dropped - return out + sizes[line], states[line] = None, kStateTimeout # recorded, never silently dropped + continue + if r.returncode != 0: + # A refusal is a DISTINCT state, never a byte count of zero. Most of these are refusals the + # corpus deliberately contains (--callers=DoesNotExist, --rank-by=bogus); they belong in the + # corpus and they do not belong in any denominator. + sizes[line], states[line] = None, state_refused(r.returncode) + else: + sizes[line], states[line] = len(r.stdout), kStateOk + return sizes, states # ── the records: TSV, because the harness must not enter the index it measures ─────────────────────── # The corpus-freeze assertion below keeps this harness out of the TREE being measured. It says nothing @@ -186,15 +290,22 @@ def parse_bytes(s, where): try: return int(s) except ValueError: sys.exit('capsweep: %s: %r is not a byte count' % (where, s)) -def write_records(path, what, columns, rows, measured_at): - """One `#` provenance line naming the columns and the measured commit, then tab-separated data.""" +def write_records(path, what, columns, rows, measured_at, recipe=()): + """`#` provenance lines naming the columns, the measured commit and the RECIPE, then the data. + + `recipe` is not decoration. A published ratio whose denominator is unstated is one list counted four + defensible ways: the round that published "59 of 195" was counting 56 rows that emit nothing at all + into the half that "responds to NO cap". The recipe travels with the records so the next reader does + not have to re-derive which population a number was over. + """ for r in rows: for f in r: if '\t' in str(f) or '\n' in str(f): sys.exit('capsweep: field %r holds a tab or newline — it cannot be a TSV record' % (f,)) - head = '# capsweep %s — columns: %s — measured_at=%s — %s' % ( - what, ' / '.join(columns), measured_at or 'unrecorded', kRecordNote) - path.write_text('\n'.join([head] + ['\t'.join(str(f) for f in r) for r in rows]) + '\n') + head = ['# capsweep %s — columns: %s — measured_at=%s — %s' % ( + what, ' / '.join(columns), measured_at or 'unrecorded', kRecordNote)] + head += ['# %s' % line for line in recipe] + path.write_text('\n'.join(head + ['\t'.join(str(f) for f in r) for r in rows]) + '\n') def read_records(path, ncol): """(measured_at, rows). `#` lines are provenance, not data — the rule corpus.txt already follows. @@ -218,7 +329,7 @@ def read_records(path, ncol): return at, rows kTunableCols = ('kind', 'value') -kScreenCols = ('baseline_bytes', 'all_bumped_bytes', 'sensitive', 'invocation') +kScreenCols = ('baseline_bytes', 'all_bumped_bytes', 'sensitive', 'baseline_state', 'all_bumped_state', 'invocation') kSweepCols = ('cap', 'value', 'probe', 'site', 'default_bytes', 'probe_bytes', 'invocation') def write_tunable(path, made, exclude, measured_at, corpus=None): @@ -237,19 +348,21 @@ def read_tunable(path): else: sys.exit('capsweep: %s: unknown kind %r' % (rel(path), kind)) return meta -def write_screen(path, corpus, base, allb, sens, measured_at): +def write_screen(path, corpus, base, allb, sens, measured_at, bstate, gstate, recipe=()): hot = set(sens) - rows = [(fmt_bytes(base.get(c)), fmt_bytes(allb.get(c)), 1 if c in hot else 0, c) for c in corpus] - write_records(path, 'screen', kScreenCols, rows, measured_at) + rows = [(fmt_bytes(base.get(c)), fmt_bytes(allb.get(c)), 1 if c in hot else 0, + bstate.get(c, '?'), gstate.get(c, '?'), c) for c in corpus] + write_records(path, 'screen', kScreenCols, rows, measured_at, recipe) def read_screen(path): at, rows = read_records(path, len(kScreenCols)) - base, allb, sens = {}, {}, [] - for b, g, s, cmd in rows: + base, allb, sens, bst = {}, {}, [], {} + for b, g, s, bs, gs, cmd in rows: base[cmd] = parse_bytes(b, rel(path)) allb[cmd] = parse_bytes(g, rel(path)) + bst[cmd] = bs if s == '1': sens.append(cmd) - return {'baseline': base, 'all_bumped': allb, 'sensitive': sens} + return {'baseline': base, 'all_bumped': allb, 'sensitive': sens, 'baseline_state': bst} def write_sweep(path, sweep, measured_at): rows = [(cap, sweep[cap]['value'], sweep[cap]['probe'], sweep[cap]['site'], b, g, cmd) @@ -290,9 +403,143 @@ def assert_corpus_clean(corpus): sys.exit('capsweep: REFUSING to measure a corpus that contains the harness measuring it: %s\n' ' (this is the 18-21-verbs-move artifact; see assert_corpus_clean)' % ', '.join(str(corpus / d) for d in inside)) + assert_no_git_above(corpus) return corpus +def assert_no_git_above(corpus): + """No `.git` in any STRICT ancestor of the corpus. The corpus's own `.git` is the fixture; anything + above it is somebody else's repository. + + ripwire walks UP the directory chain looking for `.git` (src/gitmine.h:2792, src/ingest_crawl.h:929) + and honours no ceiling variable. A frozen corpus sitting inside a checkout therefore measures THAT + checkout's history: the round of 2026-09-10 recorded `. --stray-content=lane/ --plan` at 11,670,369 B + on a corpus produced by `git archive`, which has no branches at all. Eleven megabytes of somebody + else's branch names, recorded as a cap measurement. + + This is not covered by the harness-in-corpus check above: that one looks INSIDE the corpus and this + failure is entirely OUTSIDE it. GIT_CEILING_DIRECTORIES (set in run_corpus) confines the `git` + processes ripwire spawns; only this scan confines ripwire's own walk. + """ + d = pathlib.Path(corpus).resolve().parent + while True: + if (d / '.git').exists(): + sys.exit('capsweep: REFUSING to measure a corpus with a git repository ABOVE it: %s\n' + ' ripwire walks up for .git, so every git verb would measure that\n' + ' repository instead of the frozen corpus. Point --scratch outside it.' + % (d / '.git')) + if d.parent == d: + return + d = d.parent + +# ── the corpus must not change while it is being measured ─────────────────────────────────────────── +# assert_corpus_clean guards ONE hardcoded directory name against an unbounded class. The class is what +# actually bit: `--cache=$RIPWIRE_CAPSWEEP_TMP` with the variable unexpanded wrote a 10.4 MB cache blob +# into the frozen corpus mid-sweep, and `--batch=` read it back. A name-based assertion could never have +# seen it. A file LIST taken after the freeze and re-checked after every arm sees any of it. +FINGERPRINT = 'corpus.filelist' # lives in --scratch, never in the corpus + +def fingerprint_corpus(corpus): + """The corpus's file list, `.git/` excluded. + + `.git/` is excluded deliberately and it is the one place a git verb may legitimately write: + reading a repository refreshes the index stat cache and can write ORIG_HEAD or a reflog. Those are + git's bookkeeping about the fixture, not the tree being measured. Everything else is the subject. + """ + corpus = pathlib.Path(corpus) + out = [] + for p in corpus.rglob('*'): + rp = p.relative_to(corpus) + if rp.parts and rp.parts[0] == '.git': + continue + if p.is_file() or p.is_symlink(): + out.append(str(rp)) + return sorted(out) + +def write_fingerprint(scratch, corpus): + pathlib.Path(scratch, FINGERPRINT).write_text('\n'.join(fingerprint_corpus(corpus)) + '\n') + +def read_fingerprint(scratch): + f = pathlib.Path(scratch, FINGERPRINT) + if not f.exists(): + sys.exit('capsweep: %s is missing — the corpus was never fingerprinted.\n' + ' Re-run `prepare`; a corpus nobody took a fingerprint of cannot be shown to\n' + ' have held still while it was measured.' % f) + return [l for l in f.read_text().splitlines() if l] + +def assert_corpus_unchanged(corpus, before, where): + now = fingerprint_corpus(corpus) + new = sorted(set(now) - set(before)) + gone = sorted(set(before) - set(now)) + if new or gone: + lines = ['capsweep: the frozen corpus CHANGED during %s — every byte count in this run is a' % where, + ' measurement of the harness as much as of the subject.'] + lines += [' + %s' % f for f in new[:20]] + lines += [' - %s' % f for f in gone[:20]] + if len(new) + len(gone) > 40: + lines.append(' (%d more)' % (len(new) + len(gone) - 40)) + sys.exit('\n'.join(lines)) + # ── phases ────────────────────────────────────────────────────────────────────────────────────────── +# The files the synthetic history touches. Four, and all four are prose: a marker line appended to a +# markdown file adds no symbol to the map, so the perturbation this fixture costs the OTHER 190 corpus +# rows is four lines of comment. Chosen over src/ headers for exactly that reason. +FIXTURE_FILES = ('CONTRIBUTING.md', 'docs/ARCHITECTURE.md', 'docs/METHODOLOGY.md', 'docs/EVALS.md') +FIXTURE_MARK = '' + +def plant_history(corpus): + """Give the frozen corpus a real, tiny history — because `git archive HEAD` leaves none. + + Without this, every git-dependent row in the corpus measures its DEGRADED path and says nothing + about any cap: `--handoff` reports `changed="0"` with no rows at all, and `--cochange`, `--situ`, + `--map-diff`, `--quality-delta`, `--rank-by=churn` and `--merge-scout` exit 1 with 0 bytes. Roughly + twenty corpus rows, scored as "responds to NO cap" while measuring a refusal. + + Three commits and one uncommitted edit, all over the same four files, is the smallest shape that + gives each of those verbs its real path: >1 commit for a diff, the SAME files twice for a co-change + pair, and a dirty working tree for the verbs that default to `git diff`. + + It does NOT exercise per-file symbol caps (kHandoffSymbolsPerFile and its kind): one appended line + is one changed symbol, and a cap of 6 never fires on that. Those need a real diff of a real commit, + which is a different instrument — do not read a per-file cap's silence in the sweep as evidence. + """ + env = dict(os.environ) + env.update({'GIT_AUTHOR_NAME': 'capsweep', 'GIT_AUTHOR_EMAIL': 'capsweep@invalid', + 'GIT_COMMITTER_NAME': 'capsweep', 'GIT_COMMITTER_EMAIL': 'capsweep@invalid', + 'GIT_AUTHOR_DATE': '2001-01-01T00:00:00+00:00', + 'GIT_COMMITTER_DATE': '2001-01-01T00:00:00+00:00', + 'GIT_CONFIG_GLOBAL': os.devnull, 'GIT_CONFIG_SYSTEM': os.devnull}) + present = [f for f in FIXTURE_FILES if (corpus / f).exists()] + if len(present) < 2: + sys.exit('capsweep: the history fixture found %d of its %d files in the corpus — it would plant\n' + ' an empty history and every git row would still measure a refusal.\n' + ' Update FIXTURE_FILES: %s' % (len(present), len(FIXTURE_FILES), + ', '.join(FIXTURE_FILES))) + def git(*args, **kw): + r = subprocess.run(['git', '-C', str(corpus)] + list(args), capture_output=True, text=True, env=env) + if r.returncode != 0 and not kw.get('soft'): + sys.exit('capsweep: history fixture: git %s failed:\n%s%s' % (' '.join(args), r.stdout, r.stderr)) + return r + git('init', '-q') + git('symbolic-ref', 'HEAD', 'refs/heads/main') + git('add', '-A') + git('-c', 'user.name=capsweep', '-c', 'user.email=capsweep@invalid', + 'commit', '-q', '-m', 'capsweep fixture: the frozen corpus') + for n in (2, 3): + for f in present: + with (corpus / f).open('a') as fh: + fh.write(FIXTURE_MARK % n + '\n') + git('-c', 'user.name=capsweep', '-c', 'user.email=capsweep@invalid', + 'commit', '-q', '-a', '-m', 'capsweep fixture: commit %d over the same files' % n) + for f in present: # left UNCOMMITTED: the verbs that default to `git diff` + with (corpus / f).open('a') as fh: + fh.write(FIXTURE_MARK % 4 + '\n') + head = git('rev-list', '--count', 'HEAD').stdout.strip() + dirty = git('diff', '--name-only').stdout.split() + if head != '3' or len(dirty) != len(present): + sys.exit('capsweep: history fixture planted %s commit(s) and %d dirty file(s) — expected 3 and %d' + % (head, len(dirty), len(present))) + return len(present) + def freeze_corpus(scratch): """A corpus that CANNOT move while we measure it. Tracked files only, frozen at a commit.""" corpus = scratch / 'corpus-tree' @@ -303,6 +550,9 @@ def freeze_corpus(scratch): subprocess.run(['tar', '-x', '-C', str(corpus)], input=tar.stdout, check=True) for d in HARNESS_IN_CORPUS: # the harness is tracked now — prune it back out if (corpus / d).exists(): shutil.rmtree(corpus / d) + assert_corpus_clean(corpus) # ancestor scan included — BEFORE we plant a .git + n = plant_history(corpus) + print('planted a 3-commit history over %d file(s) — the git verbs measure their real path' % n) return assert_corpus_clean(corpus) SCRATCH_STAMP = '.capsweep-scratch' # written into every scratch dir we create; required before we delete one @@ -354,18 +604,86 @@ def cmd_prepare(a): else: sys.exit('capsweep: could not converge on a buildable tunable tree') corpus = freeze_corpus(scratch) - print('frozen corpus at %s (tracked files only, from git archive HEAD)' % corpus) + write_fingerprint(scratch, corpus) + print('frozen corpus at %s (tracked files only, from git archive HEAD) — %d files fingerprinted' + % (corpus, len(read_fingerprint(scratch)))) ref = subprocess.run(['git', '-C', str(REPO), 'rev-parse', 'HEAD'], capture_output=True, text=True) out = pathlib.Path(a.scratch) / 'tunable.tsv' write_tunable(out, made, exclude, ref.stdout.strip(), corpus) print('wrote %s' % out) +def census(corpus, sizes, states): + """The four states a corpus row can be in. `ok` is the only one that carries a byte count.""" + ok = [c for c in corpus if answered(sizes, states, c)] + unp = [c for c in corpus if states.get(c, '').startswith(kStateUnparseable)] + unx = [c for c in corpus if states.get(c, '').startswith(kStateUnexpanded)] + to = [c for c in corpus if states.get(c) == kStateTimeout] + ref = [c for c in corpus if states.get(c, '').startswith('rc=')] + zero = [c for c in corpus if states.get(c) == kStateOk and (sizes.get(c) or 0) == 0] + return ok, unp, unx, to, ref, zero + +def screen_core(binary, croot, corpus, bump, out_path, measured_at, before): + """Both arms, the executability census, the refusal, and the split — over the ANSWERING rows. + + Two rules live here and nowhere else: + + 1. A run in which no row answered must not report anything. The harness used to print + `cap-sensitive: 0 (0%)` and exit 0 for a corpus that was 100% inert — a green result from an + instrument that measured nothing, which is how a whole round's worth of retrieval rows passed + unnoticed. Refusing costs one `if`; not refusing cost a night. + 2. The denominator is the rows that ANSWERED, not every row in the file. 56 of the 195 rows never + produce an answer under any cap (deliberate refusals like --callers=DoesNotExist, plus rows the + corpus harvest truncated). A row that emits nothing cannot respond to a cap, and counting it in + the half that "responds to NO cap" inflated that half by 56. + """ + base, bstate = run_corpus(binary, croot, corpus, {}) + assert_corpus_unchanged(croot, before, 'the baseline arm') + allb, gstate = run_corpus(binary, croot, corpus, bump) + assert_corpus_unchanged(croot, before, 'the all-bumped arm') + + ok, unp, unx, to, ref, zero = census(corpus, base, bstate) + print('EXECUTABILITY (baseline arm): %d/%d answered | %d unparseable | %d unexpanded variable | ' + '%d timed out | %d refused (non-zero exit) | %d exit 0 with 0 bytes' + % (len(ok), len(corpus), len(unp), len(unx), len(to), len(ref), len(zero))) + # The state is printed in FULL. Truncating it to a column width hid which variable was unexpanded, + # which is the whole content of that row's finding. + for c in unp + unx: print(' %-28s %s' % (bstate[c], c[:88])) + for c in to: print(' %-28s %s' % (kStateTimeout, c[:88])) + for c in zero: print(' %-28s %s' % ('ok but 0 bytes', c[:88])) + if not ok: + sys.exit('capsweep: 0 of %d rows produced an answer — REFUSING to write records or report a\n' + ' split. A ratio over a population that measured nothing is not a result.' + % len(corpus)) + + sens = sorted(c for c in corpus if base.get(c) != allb.get(c)) + answ = set(ok) + hot = [c for c in sens if c in answ] + late = [c for c in sens if c not in answ] # answered ONLY under the bumped arm — real signal + recipe = ('split recipe: DENOMINATOR = rows that answered under the BASELINE arm (state=ok, >0 bytes).', + 'A row that emits nothing cannot respond to a cap; %d row(s) of %d never answer and are' + % (len(corpus) - len(ok), len(corpus)), + 'recorded here but excluded from the ratio.', + 'cap-sensitive=%d of %d answering (%.0f%%); %d row(s) answer only when a cap is bumped.' + % (len(hot), len(ok), 100.0 * len(hot) / len(ok), len(late))) + write_screen(out_path, corpus, base, allb, sens, measured_at, bstate, gstate, recipe) + print('corpus %d — %d answered — cap-sensitive: %d of %d answering rows (%.0f%%); the other %d ' + 'answering rows respond to NO cap' + % (len(corpus), len(ok), len(hot), len(ok), 100.0 * len(hot) / len(ok), len(ok) - len(hot))) + for c in late: + print(' %8s %s' % ('BY-CAP', c[:88])) # refused at the default, answers when bumped + for c in hot[:15]: + if base.get(c) is None or allb.get(c) is None: + print(' %8s %s' % ('TIMEOUT', c[:88])) + else: + print(' %+8d B %s' % (allb[c] - base[c], c[:88])) + return sens + def cmd_screen(a): scratch = pathlib.Path(a.scratch); binary = scratch / 'build' / 'ripwire' meta = read_tunable(scratch / 'tunable.tsv') corpus = read_corpus() croot = assert_corpus_clean(meta.get('corpus') or scratch / 'corpus-tree') # re-checked per phase - base = run_corpus(binary, croot, corpus, {}) + before = read_fingerprint(scratch) # RELATIVE bump, not a flat huge value: several of these are query/work budgets in the 10^5 range, # and slamming them all to 999999 makes the screen measure the machine rather than the cap. vals = {c[0]: c[1] for c in caps_in(REPO)} @@ -374,32 +692,24 @@ def bumped(n): except ValueError: v = 8.0 return ('%g' % max(v * 8.0, v + 32.0)) if v == int(v) else ('%g' % min(v * 4.0, 1.0)) bump = {('RWCAP_%s' % n): bumped(n) for n in meta['tunable']} - allb = run_corpus(binary, croot, corpus, bump) - # run_corpus records a timeout as None, on purpose ("recorded, never silently dropped"). A row - # where exactly one arm timed out therefore DIFFERS and lands in `sens`, and the delta print below - # would then subtract None. Keep it in the sensitive set -- a timeout under one arm and not the - # other is real signal -- but let it carry the word TIMEOUT instead of crashing the screen. - sens = sorted(c for c in corpus if base.get(c) != allb.get(c)) - write_screen(scratch / 'screen.tsv', corpus, base, allb, sens, meta.get('measured_at')) - print('corpus %d — cap-sensitive: %d (%.0f%%); the other %d respond to NO cap' - % (len(corpus), len(sens), 100.0*len(sens)/len(corpus), len(corpus)-len(sens))) - for c in sens[:15]: - if base.get(c) is None or allb.get(c) is None: - print(' %8s %s' % ('TIMEOUT', c[:88])) - else: - print(' %+8d B %s' % (allb[c]-base[c], c[:88])) + screen_core(binary, croot, corpus, bump, scratch / 'screen.tsv', meta.get('measured_at'), before) def cmd_sweep(a): """Per cap: which of the sensitive commands actually respond to THIS cap, and by how much. - Only the 59 cap-sensitive commands are used. The other 136 answered identically with every cap - bumped at once, so no single cap can move them — running them per-cap would be 16,000 wasted runs. + Only the cap-sensitive commands are used. The rest answered identically with every cap bumped at + once, so no single cap can move them — running them per-cap would be 16,000 wasted runs. + + The corpus fingerprint is re-checked after EVERY cap's arm, not once at the end: the failure this + guards against (the harness writing into the corpus it measures) is cumulative, and the arm that + created the file is the one worth naming. """ scratch = pathlib.Path(a.scratch); binary = scratch / 'build' / 'ripwire' meta = read_tunable(scratch / 'tunable.tsv') screen = read_screen(scratch / 'screen.tsv') sens, base = screen['sensitive'], screen['baseline'] croot = assert_corpus_clean(meta.get('corpus') or scratch / 'corpus-tree') # re-checked per phase + before = read_fingerprint(scratch) vals = {c[0]: c[1] for c in caps_in(REPO)} sites = {c[0]: '%s:%d' % (c[2], c[3]) for c in caps_in(REPO)} out = {} @@ -409,7 +719,8 @@ def cmd_sweep(a): if v != int(v) or v <= 0: # ladders only make sense for integral counts continue hi = '%d' % max(int(v) * 8, int(v) + 32) - got = run_corpus(binary, croot, sens, {'RWCAP_%s' % cap: hi}) + got, gstate = run_corpus(binary, croot, sens, {'RWCAP_%s' % cap: hi}) + assert_corpus_unchanged(croot, before, 'the %s arm' % cap) moved = {c: (base[c], got[c]) for c in sens if got.get(c) is not None and base.get(c) is not None and got[c] != base[c]} if moved: @@ -549,22 +860,64 @@ def cmd_patch(a): def cmd_checkcorpus(a): assert_corpus_clean(a.corpus) - print('capsweep: corpus %s is clean of the harness' % rel(a.corpus)) + print('capsweep: corpus %s is clean of the harness and has no git repository above it' % rel(a.corpus)) + +def cmd_planthistory(a): + """Plant the history fixture in --root and stop, so the gate can drive it without a full prepare.""" + root = pathlib.Path(a.root).resolve() + if root == REPO: + sys.exit('capsweep: refusing to plant a fixture history in the repository itself') + n = plant_history(root) + print('capsweep: planted a 3-commit fixture history over %d file(s) in %s' % (n, rel(root))) + +def cmd_runcorpus(a): + """Both screen arms against an ARBITRARY binary and corpus, and stop. + + This exists so test/capsweepcheck.sh can drive the real screen_core — the census, the refusal, the + denominator and the corpus fingerprint — against a stub binary and a six-row synthetic corpus, + WITHOUT a patched build. Every one of those rules was added because it had already failed silently + once; a rule whose gate cannot go red is a comment. + """ + croot = assert_corpus_clean(a.corpus) + corpus = [l for l in pathlib.Path(a.corpus_file).read_text().splitlines() + if l.strip() and not l.lstrip().startswith('#')] + bump = {} + for kv in (a.bump or []): + if '=' not in kv: + sys.exit('capsweep: --bump takes NAME=VALUE, got %r' % kv) + k, v = kv.split('=', 1) + bump[k] = v + before = fingerprint_corpus(croot) + # NOT --out: that flag already means "the document emit writes" and defaults to docs/TUNING.md, so + # reusing it here would overwrite the generated document with a record file. + screen_core(pathlib.Path(a.binary).resolve(), croot, corpus, bump, + pathlib.Path(a.records or (pathlib.Path(a.corpus).parent / 'screen.tsv')), + 'synthetic', before) if __name__ == '__main__': ap = argparse.ArgumentParser() - ap.add_argument('phase', choices=['prepare', 'screen', 'sweep', 'emit', 'patch', 'check-corpus']) + ap.add_argument('phase', choices=['prepare', 'screen', 'sweep', 'emit', 'patch', 'check-corpus', + 'run-corpus', 'plant-history']) ap.add_argument('--scratch', default=str(pathlib.Path.home() / '.cache' / 'ripwire-capsweep')) ap.add_argument('--jobs', type=int, default=8) ap.add_argument('--data', default=str(HERE), help='emit: dir holding the frozen tunable/sweep TSV') ap.add_argument('--out', default=str(REPO / 'docs' / 'TUNING.md'), help='emit: the document to write') ap.add_argument('--check', action='store_true', help='emit: compare instead of writing; exit 1 on drift') ap.add_argument('--root', default=None, help='patch: the SCRATCH tree to rewrite (never the repo)') - ap.add_argument('--corpus', default=None, help='check-corpus: the frozen corpus to assert on') + ap.add_argument('--corpus', default=None, help='check-corpus/run-corpus: the frozen corpus') + ap.add_argument('--binary', default=None, help='run-corpus: the binary (or stub) to run') + ap.add_argument('--corpus-file', default=None, help='run-corpus: the invocation list to run') + ap.add_argument('--bump', action='append', default=None, help='run-corpus: NAME=VALUE for the bumped arm') + ap.add_argument('--records', default=None, help='run-corpus: where to write the screen records') a = ap.parse_args() if a.phase == 'patch' and not a.root: ap.error('patch requires --root TREE') if a.phase == 'check-corpus' and not a.corpus: ap.error('check-corpus requires --corpus DIR') + if a.phase == 'run-corpus' and not (a.corpus and a.binary and a.corpus_file): + ap.error('run-corpus requires --binary, --corpus and --corpus-file') + if a.phase == 'plant-history' and not a.root: + ap.error('plant-history requires --root TREE') {'prepare': cmd_prepare, 'screen': cmd_screen, 'sweep': cmd_sweep, 'emit': cmd_emit, - 'patch': cmd_patch, 'check-corpus': cmd_checkcorpus}[a.phase](a) + 'patch': cmd_patch, 'check-corpus': cmd_checkcorpus, 'run-corpus': cmd_runcorpus, + 'plant-history': cmd_planthistory}[a.phase](a) diff --git a/test/capsweepcheck.sh b/test/capsweepcheck.sh index 92e2e1596..f542887be 100755 --- a/test/capsweepcheck.sh +++ b/test/capsweepcheck.sh @@ -41,6 +41,22 @@ # margin_pct="22" without, and `--for=kMaxExpandSibs` surfaced bench/capsweep/sweep.json in an # answer about a cap. So a json file under bench/capsweep is a failure by its extension alone, # held down by a control that finds one in a SYNTHETIC copy — never in the real tree. +# (G) AN UNBALANCED QUOTE is recorded UNPARSEABLE and the rows after it still run. +# (H) A NON-ZERO EXIT is a distinct state carrying no byte count — never "0 bytes", which is what made +# a refusal and an answer-of-nothing the same measurement. +# (I) THE DENOMINATOR is the rows that ANSWERED, the records say so, and a run in which NOTHING +# answered REFUSES to report a split or write records (the green-while-inert class). +# (J) $VARS expand from the environment handed to the child, an UNDEFINED one is refused rather than +# passed through as a literal, and the destination resolves outside the corpus. +# (K) A CORPUS FINGERPRINT taken before the arms and re-checked after each one: a file created inside +# the corpus mid-run aborts and names the path. (B) guards one directory name; this guards the class. +# (L) A GIT REPOSITORY ABOVE the corpus is refused — ripwire walks up for .git in its own code. +# (M) THE HISTORY FIXTURE: `git archive HEAD` leaves no .git, so the git verbs measured their degraded +# path. Three commits over the same files plus a dirty tree; a tree missing them is REFUSED. +# +# G-L run the production screen_core through the `run-corpus` phase against a STUB binary, so they still +# cost no build. Arms A-F never executed a single corpus row, which is precisely how four defects +# shipped inside the phase they were meant to guard. # # This gate binds no ripwire binary: its subjects are a python harness, a source tree and a markdown # file. It is pinned in test/binoverridecheck.sh's exemption list for that reason. @@ -179,5 +195,205 @@ case "$( jsonrecords "$TMP/synthjson" )" in *) no "(F) control: the scan did not see a json record in a synthetic copy — (F) is inert" ;; esac +# ── (G..K) THE HARNESS'S OWN HONESTY, driven through the real screen_core with a stub binary ───────── +# Arms A-F are source-level and none of them ever executes a corpus row, which is exactly how four +# defects shipped inside the phase they were supposed to guard: shlex raising on an unbalanced quote +# (a hard crash of `screen`), a refusal recorded as "0 bytes" and indistinguishable from an answer, a +# 100%-inert run printing a clean 0% and exiting 0, and $RIPWIRE_CAPSWEEP_TMP expanded from os.environ +# instead of the child env so nine rows wrote a 10.4 MB cache blob INTO the frozen corpus. +# +# `run-corpus` runs the production screen_core against a STUB binary, so these arms cost no build. +mkdir -p "$TMP/stub" "$TMP/rc/src" +cat > "$TMP/stub/ripwire" <<'STUBEOF' +#!/bin/sh +# A stand-in for the binary: enough behaviour to exercise every row state the harness must tell apart. +for a in "$@"; do + case "$a" in + --stub-ok) printf '%0100d' 0; exit 0 ;; + --stub-cap) if [ -n "${RWCAP_kStubRowCap:-}" ]; then printf '%0400d' 0; else printf '%0100d' 0; fi; exit 0 ;; + --stub-refuse) exit 3 ;; + --stub-tmp=*) d="${a#--stub-tmp=}"; mkdir -p "$d" 2>/dev/null; : > "$d/wrote-here"; printf 'tmp=%s' "$d"; exit 0 ;; + --stub-litter) : > "capsweep-litter.txt"; printf '%050d' 0; exit 0 ;; + esac +done +printf 'x'; exit 0 +STUBEOF +chmod +x "$TMP/stub/ripwire" +: > "$TMP/rc/src/a.h" +cat > "$TMP/rc/corpus.txt" <<'CORPEOF' +# a synthetic corpus: one row per state the harness has to distinguish +. --stub-ok +. --stub-cap +. --stub-refuse +. --stub-unbalanced="oops +. --stub-tmp=$RIPWIRE_CAPSWEEP_TMP +. --stub-undefined=$CAPSWEEP_NO_SUCH_VAR +CORPEOF +rc_out="$TMP/rc.out" +# env -u, not `VAR=`: an empty binding is not the operator's normal case, and it used to resolve to the +# process CWD — the run then wrote into the checkout it was launched from. +if env -u RIPWIRE_CAPSWEEP_TMP python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rc" \ + --corpus-file "$TMP/rc/corpus.txt" --bump RWCAP_kStubRowCap=64 \ + --records "$TMP/rc-screen.tsv" > "$rc_out" 2>&1; then + g_ok=1 +else + g_ok=0 +fi +if [ "$g_ok" != 1 ]; then + no "(G) run-corpus failed on the synthetic corpus: $( tail -3 "$rc_out" | tr '\n' ' ' )" +else + # (G) the unbalanced-quote row is UNPARSEABLE, and the rows AFTER it still ran. The second half is + # the F1b control: `except ValueError as e` shadows run_corpus's env dict `e`, and Python deletes an + # except-name at block end, so the obvious repair kills the NEXT row with UnboundLocalError. + if grep -q 'unparseable' "$rc_out" && grep -Eq '^EXECUTABILITY.*: 3/6 answered' "$rc_out"; then + ok "(G) an unbalanced quote is recorded UNPARSEABLE and the rows after it still run" + else + no "(G) unparseable row not classified, or the rows after it did not run: $( grep -m1 EXECUTABILITY "$rc_out" )" + fi + # (H) a refusal is a state, never a byte count of zero. + if grep -q 'rc=3' "$TMP/rc-screen.tsv" && \ + awk -F'\t' '/--stub-refuse/ { exit !($1 == "-" && $4 == "rc=3") }' "$TMP/rc-screen.tsv"; then + ok "(H) a non-zero exit is recorded as rc=3 with NO byte count, not as 0 bytes" + else + no "(H) the refusing row was not recorded as a distinct state: $( grep -- '--stub-refuse' "$TMP/rc-screen.tsv" )" + fi + # (I) the denominator is the ANSWERING rows: 1 of 3, never 1 of 6. + if grep -q 'cap-sensitive: 1 of 3 answering rows' "$rc_out"; then + ok "(I) the split is reported over the 3 answering rows, not over all 6" + else + no "(I) the split was not reported over the answering rows: $( grep -m1 'cap-sensitive' "$rc_out" )" + fi + if grep -q 'split recipe: DENOMINATOR' "$TMP/rc-screen.tsv"; then + ok "(I) the records carry the recipe the ratio was computed by" + else + no "(I) the screen records do not state their denominator — a published ratio with an unstated recipe" + fi + # (J) $VARS expand from the environment the harness hands the child, and an UNDEFINED one is refused + # rather than passed through as a literal path (that literal is what wrote 10.4 MB into the corpus). + if grep -q 'unexpanded: \$CAPSWEEP_NO_SUCH_VAR' "$rc_out"; then + ok "(J) a row naming an undefined variable is REFUSED, not run with the literal \$NAME" + else + no "(J) an undefined variable was passed through as a literal — the F17 shape" + fi + if [ -f "$TMP/corpus-tmp/wrote-here" ] && [ ! -e "$TMP/rc/corpus-tmp" ]; then + ok "(J) \$RIPWIRE_CAPSWEEP_TMP expanded from the child env, to a path OUTSIDE the corpus" + else + no "(J) the corpus-tmp destination was not written outside the corpus: $( grep -m1 stub-tmp "$rc_out" )" + fi +fi + +# (J) control — a destination that resolves INSIDE the corpus is refused. `--cache=`, `--export=` and +# `--html=` all take one, and run_corpus runs with cwd=corpus, so this is the surface that put a 10.4 MB +# cache blob in the frozen tree. +if out="$( RIPWIRE_CAPSWEEP_TMP="$TMP/rc/inside" python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" \ + --corpus "$TMP/rc" --corpus-file "$TMP/rc/corpus.txt" --records "$TMP/rcin.tsv" 2>&1 )"; then + no "(J) control: a scratch destination INSIDE the corpus was accepted — rows would write into the subject" +else + case "$out" in + *'resolves INSIDE the corpus'*) ok "(J) control: a scratch destination inside the corpus is refused" ;; + *) no "(J) control: the inside-the-corpus run failed for the wrong reason: $( echo "$out" | tail -2 | tr '\n' ' ' )" ;; + esac +fi + +# (I) control — a corpus in which NOTHING answers must refuse, not print a clean 0%. +mkdir -p "$TMP/rcinert/src"; : > "$TMP/rcinert/src/a.h" +cat > "$TMP/rcinert/corpus.txt" <<'CORPEOF' +. --stub-refuse +. --stub-unbalanced="oops +CORPEOF +if out="$( python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rcinert" \ + --corpus-file "$TMP/rcinert/corpus.txt" --records "$TMP/rcinert-screen.tsv" 2>&1 )"; then + no "(I) control: a corpus where NO row answered still reported a split and exited 0 — green-while-inert" +elif [ -f "$TMP/rcinert-screen.tsv" ]; then + no "(I) control: the inert run refused but still WROTE records — a record of a measurement that did not happen" +else + case "$out" in + *'REFUSING to write records'*) ok "(I) control: a run in which no row answered refuses and writes nothing" ;; + *) no "(I) control: the inert run failed for the wrong reason: $( echo "$out" | tail -2 | tr '\n' ' ' )" ;; + esac +fi + +# (K) the corpus fingerprint: a file created INSIDE the corpus mid-run aborts the run and names the path. +# assert_corpus_clean guards one hardcoded directory name; this guards the class. +mkdir -p "$TMP/rclitter/src"; : > "$TMP/rclitter/src/a.h" +cat > "$TMP/rclitter/corpus.txt" <<'CORPEOF' +. --stub-ok +. --stub-litter +CORPEOF +if out="$( python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rclitter" \ + --corpus-file "$TMP/rclitter/corpus.txt" --records "$TMP/rclitter-screen.tsv" 2>&1 )"; then + no "(K) a row that wrote a file into the corpus was measured anyway — the fingerprint cannot fire" +else + case "$out" in + *'corpus CHANGED'*capsweep-litter.txt*) + ok "(K) a file created inside the corpus mid-run aborts the run and names the path" ;; + *) no "(K) the litter run failed for the wrong reason: $( echo "$out" | tail -2 | tr '\n' ' ' )" ;; + esac +fi +# and the control: the SAME corpus without the littering row must be measured, or (K) refuses everything +cat > "$TMP/rclitter/corpus.txt" <<'CORPEOF' +. --stub-ok +CORPEOF +rm -f "$TMP/rclitter/capsweep-litter.txt" +if python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rclitter" \ + --corpus-file "$TMP/rclitter/corpus.txt" --records "$TMP/rclitter-screen2.tsv" >/dev/null 2>&1; then + ok "(K) control: a corpus that holds still is measured — the fingerprint is not refusing everything" +else + no "(K) control: a corpus that did NOT change was refused, so (K)'s refusals mean nothing" +fi + +# ── (L) a git repository ABOVE the corpus is refused ──────────────────────────────────────────────── +# ripwire walks UP for .git in its own code and honours no ceiling variable, so a frozen corpus inside +# somebody's checkout measures THAT checkout: the 2026-09-10 round recorded `--stray-content=lane/ --plan` +# at 11,670,369 B on a `git archive` corpus, which has no branches at all. +mkdir -p "$TMP/anc/.git" "$TMP/anc/inner/corpus/src"; : > "$TMP/anc/inner/corpus/src/a.h" +if python3 "$GEN" check-corpus --corpus "$TMP/anc/inner/corpus" >/dev/null 2>&1; then + no "(L) a corpus with a git repository two levels ABOVE it was ACCEPTED — every git verb would measure it" +else + rm -rf "$TMP/anc/.git" + if python3 "$GEN" check-corpus --corpus "$TMP/anc/inner/corpus" >/dev/null 2>&1; then + ok "(L) the ancestor scan refuses a corpus under a git repository and accepts one that is not" + else + no "(L) the ancestor scan refused a corpus with no .git above it — it rejects everything" + fi +fi +# the corpus's OWN .git is the history fixture and must not trip the ancestor scan +mkdir -p "$TMP/anc/inner/corpus/.git" +if python3 "$GEN" check-corpus --corpus "$TMP/anc/inner/corpus" >/dev/null 2>&1; then + ok "(L) the corpus's own .git — the history fixture — is not mistaken for an ancestor repository" +else + no "(L) the corpus's own .git was refused; the history fixture could never be planted" +fi + +# ── (M) the git history fixture, on a synthetic tree ──────────────────────────────────────────────── +# `git archive HEAD` produces a tree with NO .git, so every git-dependent corpus row measures its +# degraded path: --handoff reports changed="0" with no rows, and --cochange/--situ/--map-diff/ +# --quality-delta exit 1 with 0 bytes. ~20 rows scored "responds to NO cap" while measuring a refusal. +mkdir -p "$TMP/hist/docs" "$TMP/hist/src" +printf '# doc\n\ncontent\n' > "$TMP/hist/CONTRIBUTING.md" +printf '# doc\n\ncontent\n' > "$TMP/hist/docs/ARCHITECTURE.md" +printf '# doc\n\ncontent\n' > "$TMP/hist/docs/METHODOLOGY.md" +printf '# doc\n\ncontent\n' > "$TMP/hist/docs/EVALS.md" +: > "$TMP/hist/src/a.h" +if python3 "$GEN" plant-history --root "$TMP/hist" > "$TMP/hist.out" 2>&1; then + commits="$( git -C "$TMP/hist" rev-list --count HEAD 2>/dev/null )" + dirty="$( git -C "$TMP/hist" diff --name-only 2>/dev/null | wc -l | tr -d ' ' )" + if [ "$commits" = 3 ] && [ "$dirty" = 4 ]; then + ok "(M) the history fixture plants 3 commits over the same files and leaves a dirty working tree" + else + no "(M) the history fixture planted $commits commit(s) and $dirty dirty file(s) — expected 3 and 4" + fi +else + no "(M) plant-history failed: $( tail -3 "$TMP/hist.out" | tr '\n' ' ' )" +fi +# the control: a tree missing the fixture's files must REFUSE rather than plant an empty history, or +# every git row goes back to measuring a refusal with nothing saying so. +mkdir -p "$TMP/histbare/src"; : > "$TMP/histbare/src/a.h" +if python3 "$GEN" plant-history --root "$TMP/histbare" >/dev/null 2>&1; then + no "(M) control: a tree with none of the fixture's files still planted a history — the fixture is inert" +else + ok "(M) control: a tree missing the fixture's files is refused, not silently given an empty history" +fi + [ $fail -eq 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 09d4f5fc82c3d384ca316574b5e312c6e8a673b9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:10:16 -0400 Subject: [PATCH 05/73] quality(dead-code): what the LANGUAGE invokes, not what a header exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `isDeadCandidate` answered false for any symbol whose file ended .h/.hpp/.hh/.hxx — "header-exported by convention". On a header-only C++ codebase that is not a filter, it is a blindfold: 153,650 of this repo's 158,700 src LOC live in headers, so 96.8% of the source was invisible to the kind, and audit lane Q1's synthetic S6 (delete the sole caller of a header function) was silently missed. Meanwhile the 3.2% it could see produced ten rows across 40 replayed commits and ALL TEN were wrong for one reason: they named symbols the LANGUAGE invokes — `operator new`/`delete`/`delete[]`, constructors, a functor's `operator()`, a bare type — for which zero in-edges in a name-based call graph is evidence of nothing at all. The proxy is replaced by the rule it stood for. languageInvokedSymbol excludes a type (never called), `main` (the runtime calls it), `operator...` (invoked by the operator's own syntax), a leading-tilde destructor, a member sharing its type's name (a constructor in every language that spells one that way), a Python dunder, and a Method named init/deinit/constructor. Each clause names a call site the parser cannot see as a CALL, and the rule errs toward false-LIVE, the only safe direction for a deletion candidate. REF-PAIR REPLAY — 40 commits, ack-free root: | | before | after | | rows | 259 | 251 | | dead-code rows | 10 | 2 | | gating rows | 69 | 69 | | commits that gate | 20/40 | 20/40 | | gating precision TRUE | 10% | 10% | | gating precision TRUE+chronic| 71% | 71% | All ten previous dead-code rows are gone (four test-harness constructors, one bare type, five operator new/delete pairs). The two that survive are the `infra::sort::stable` overloads at 08416403 — the two Q1 labelled genuinely uncalled. Working-tree replay is unchanged (266 rows / 54 gating / 9 of 12): the kind produced zero rows there before and after. 27 of 27 TRUE-or-chronic gating rows survive in the working-tree population, 49 of 49 in the ref-pair one, 0 lost, 0 demoted. RECALL: synthetic S6 turns from a silent miss into a reported row — test/qddialscheck.sh §2 deletes the sole caller of a header function and asserts the row, beside the opposite arm that a brand-new type's ctor, dtor and operator produce NO row. Both are RED on the pre-change binary and for opposite reasons: it misses usedHelper entirely, and it reports three Extra:: rows a .cpp made visible to it. kQSnapCacheScheme 8 -> 9. The dead SET changed meaning in BOTH directions, and the direction is what makes the bump load-bearing: a v8 blob's dead set was computed while the header population was invisible, so served to this binary every newly-eligible dead symbol would read as absent from the baseline and be reported as freshly dead — a tree of phantom regressions on the first run after an upgrade. No extraction change, so kParserVer and its mirrors deliberately did not move. deadcheck, deadfiltercheck, deadprecisioncheck (the --dead-code verb runs through deadCodeEligibleKind and is untouched), registermacrocheck, qsnapcachecheck, qextractionkeycheck, qualitycheck, safedeletecheck, qddialscheck: PASS. Co-Authored-By: Claude Fable 5.1 --- src/quality.h | 83 ++++++++++++++++++++++++++++++++++++++++---- test/qddialscheck.sh | 56 ++++++++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 7 deletions(-) diff --git a/src/quality.h b/src/quality.h index 3d1059c88..097c3f4cc 100644 --- a/src/quality.h +++ b/src/quality.h @@ -475,8 +475,70 @@ inline std::vector topLevelCalleeNameHashes( const IngestResult& return hashes; } +// Q-DIAL-2 (2026-09-10) — THE SYMBOLS A LANGUAGE INVOKES, for which "zero in-edges in a name-based call +// graph" is evidence of nothing at all. This is what the dead kind's blanket header exclusion was a PROXY +// for, stated directly, and it is measurable in both directions: all ten dead-code rows the verb produced +// across 40 replayed commits were exactly these shapes (audit Q1 §2e W1), and the header rule that hid them +// also hid 96.8% of this repo's own source from the kind (Q1 §3, synthetic S6 — the sole caller of a header +// function deleted, silently missed). +// +// Each clause names a call site the parser cannot see as a named CALL: +// * a TYPE (class/struct/interface) is never invoked at all — its in-edge count is not a liveness signal; +// * `main` is invoked by the runtime; +// * `operator...` is invoked by the OPERATOR'S SYNTAX (`a + b`, `p[i]`, `new T`, `f( x )` on a functor); +// * a leading `~` is a C++ destructor — the language runs it at scope exit; +// * name == the innermost scope segment is a CONSTRUCTOR in every language that spells one that way +// (C++, Java, C#, PHP-in-part), built by object creation rather than by a call to that name; +// * a Python-style dunder (`__enter__`, `__repr__`, `__init__`) is invoked by a protocol, never by name; +// * a METHOD named init/deinit/constructor is Swift's / JavaScript's spelling of the same constructor +// protocol. Scoped to Method deliberately: a free function called `init` is an ordinary function, and +// excluding it would be the header rule's over-reach in a smaller costume. +// FLOOR, stated: this is a NAME-level rule, exactly like the resolver's own bare-name matching, and it errs +// toward false-LIVE (a symbol wrongly considered invoked is silently not reported) rather than false-dead, +// which is the direction a deletion candidate must err in. +inline bool languageInvokedSymbol( const Symbol& s ) noexcept +{ + if( s.kind == SymKind::Class || s.kind == SymKind::Struct || s.kind == SymKind::Interface ) + { + return true; // a type is declared, never called + } + if( s.name == "main" ) + { + return true; // the runtime's entry point + } + if( s.name.rfind( "operator", 0 ) == 0 ) + { + return true; // invoked by the operator's own syntax + } + if( !s.name.empty() && s.name.front() == '~' ) + { + return true; // C++ destructor + } + if( s.name.size() > 4 && s.name.rfind( "__", 0 ) == 0 + && s.name.compare( s.name.size() - 2, 2, "__" ) == 0 ) + { + return true; // Python dunder — invoked by a protocol + } + if( s.kind == SymKind::Method && ( s.name == "init" || s.name == "deinit" || s.name == "constructor" ) ) + { + return true; // Swift init/deinit, JavaScript constructor + } + if( !s.scope.empty() ) + { + const std::size_t sep = s.scope.rfind( "::" ); + const std::string_view tail = sep == std::string::npos ? std::string_view( s.scope ) + : std::string_view( s.scope ).substr( sep + 2 ); + if( !tail.empty() && tail == s.name ) + { + return true; // constructor: the member that shares its type's name + } + } + return false; +} + // A "dead deletion-candidate": has a body, no caller in the indexed tree, not invoked from file scope, not -// header-exported, not a test fixture, not produced by a registered self-registering macro. A SIMPLE, +// invoked by the LANGUAGE itself (languageInvokedSymbol, above), not a test fixture, not produced by a +// registered self-registering macro. A SIMPLE, // internally-consistent heuristic — the delta only needs baseline↔current consistency, not parity with the // fuller --dead-code verb. `topLevelCallees` is the sorted set topLevelCalleeNameHashes builds and // `registeredMacroIds` the sorted set registeredMacroSymbolIds builds (below, past forEachSymbolBody) — @@ -511,13 +573,11 @@ inline bool isDeadCandidate( const IngestResult& ing, const Graph& g, NodeId i, { return false; // W1-S2: invoked from file scope (a top-level script statement) — a use the CSR drops } - const std::string& p = ing.files[ s.fileId ]; - const auto ends = [ & ]( std::string_view e ) - { return p.size() >= e.size() && p.compare( p.size() - e.size(), e.size(), e ) == 0; }; - if( ends( ".h" ) || ends( ".hpp" ) || ends( ".hh" ) || ends( ".hxx" ) ) + if( languageInvokedSymbol( s ) ) { - return false; // header-exported by convention + return false; // Q-DIAL-2: the LANGUAGE calls it — see languageInvokedSymbol (this replaced a blanket header exclusion) } + const std::string& p = ing.files[ s.fileId ]; if( isFixturePath( p ) ) { return false; // fixtures are dead by design (noise rules) @@ -2021,7 +2081,16 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // Benchmark BENCHMARK bodies reported as newly-dead the moment an agent added a test). No extraction // change — the underlying symbols were always indexed; only the dead-SET predicate narrowed — so // kParserVer/the mirrors deliberately did NOT move. Bumped 7 -> 8 to retire every blob written before it. -constexpr std::uint32_t kQSnapCacheScheme = 8; +// v9 (Q-DIAL-2, 2026-09-10) — `isDeadCandidate`'s header exclusion was REPLACED by languageInvokedSymbol, +// so the dead SET both grew (every header symbol with no caller is now eligible) and shrank (constructors, +// destructors, operators, bare types and main are out). Same shape as v6/v8 in the opposite direction, and +// the direction is what makes the bump load-bearing rather than hygienic: a v8 blob's dead set was computed +// while 96.8% of this repo's source was invisible to the predicate, so served to this binary every +// newly-eligible dead symbol would read as ABSENT from the baseline dead set and be reported as freshly +// dead — a whole tree of phantom regressions on the first run after an upgrade. No extraction change (the +// symbols were always indexed; only the dead-SET predicate moved), so kParserVer and its mirrors deliberately +// did NOT move. Bumped 8 -> 9 to retire every blob written before it. +constexpr std::uint32_t kQSnapCacheScheme = 9; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 94e9a841c..9a651964e 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -76,5 +76,61 @@ row "$OCH" short-horizon-churn once | grep -q 'sev="minor"' \ [ "$OCH" = "$( cd "$CH" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ && ok "churn: byte-identical run to run (deterministic)" || no "churn: non-deterministic delta" +# ── 2) dead-code: the header exclusion is gone; what is exempt is what the LANGUAGE invokes ────────────── +# The kind used to answer false for ANY symbol in a .h/.hpp/.hh/.hxx file ("header-exported by convention"), +# which on this header-only codebase hid 96.8% of src from it — synthetic S6 (the sole caller of a header +# function deleted) was silently missed. The proxy is replaced by the rule it stood for: a symbol the +# language itself invokes has no named call site for the graph to record, so zero in-edges says nothing. +# +# Two arms, both red on the pre-change binary and for opposite reasons: +# the header function that LOSES its last caller must now be reported (recall); +# the constructor / destructor / operator / bare type the working tree ADDS must not be (precision) — +# before the dial those were only silent because they sat in a header, and in a .cpp they were reported. +DC="$WORK/dead"; mkdir -p "$DC/src" +( cd "$DC" && git init -q && git config user.email t@t && git config user.name t && git config commit.gpgsign false ) +printf 'inline int usedHelper(){ return 41; }\n' > "$DC/src/lib.hpp" +cat > "$DC/src/m.cpp" <<'CPP' +#include "lib.hpp" +struct Thing { + Thing() { value = 1; } + ~Thing() { value = 0; } + bool operator==( const Thing& o ) const { return value == o.value; } + int value; +}; +int driver(){ return usedHelper(); } +int main(){ Thing t; return driver() + t.value; } +CPP +( cd "$DC" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) +# the working edit: driver() stops calling usedHelper (S6 — the sole caller deleted), and a brand-new type +# arrives whose ctor, dtor and operator have no named caller anywhere. +python3 - "$DC/src/m.cpp" <<'PY' +import sys +p=sys.argv[1]; s=open(p).read() +s=s.replace("int driver(){ return usedHelper(); }","""struct Extra { + Extra() { n = 2; } + ~Extra() { n = 0; } + bool operator<( const Extra& o ) const { return n < o.n; } + int n; +}; +int driver(){ return 41; }""") +open(p,"w").write(s) +PY +ODC="$( cd "$DC" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +row "$ODC" dead-code usedHelper >/dev/null \ + && ok "dead-code: a HEADER function that lost its sole caller is reported (synthetic S6)" \ + || { no "dead-code: usedHelper not reported — the header exclusion still hides the kind"; rows "$ODC"; } +DEADROWS="$( rows "$ODC" | grep -c 'kind="dead-code"' )" +# One match on the whole family: the ctor and dtor BOTH index as Extra::Extra (the parser keeps no leading +# tilde) and the operator arrives XML-escaped as operator<, so naming them one by one greps for spellings +# that never appear. Anything under the new type is a language-invoked symbol and must not be a row. +rows "$ODC" | grep 'kind="dead-code"' | grep -q 'Extra' \ + && { no "dead-code: a language-invoked member of Extra reported (ctor/dtor/operator/type)"; rows "$ODC" | grep 'kind="dead-code"'; } \ + || ok "dead-code: no ctor/dtor/operator/type row for the new Extra type (the language invokes them)" +[ "$DEADROWS" = 1 ] && ok "dead-code: exactly ONE dead-code row on this fixture (only usedHelper)" \ + || { no "dead-code: expected 1 dead-code row, got $DEADROWS"; rows "$ODC" | grep 'kind="dead-code"'; } +[ "$ODC" = "$( cd "$DC" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "dead-code: byte-identical run to run (deterministic)" || no "dead-code: non-deterministic delta" + + [ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" exit "$fail" From c7bb0694b88856bc9e56ee9b339817f9da1622ed Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:15:18 -0400 Subject: [PATCH 06/73] fix(help-task): an intent word may not mint the symbol its own gate requires MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `does` was a symbol-slot cue AND `how does` is the understand-symbol gate, so every English question of the form "how does …?" minted the very symbol the gate then demanded — the same two words playing both parts. 13 of 25 adversarial prose prompts recommended `--expand=`, and the Codex UserPromptSubmit hook injects that answer into a live session at confidence="high" (2026-09-10 audit F-R1-01/04). Six of those thirteen names existed ONLY as t="sec" rows — JSON keys and markdown headings — so `--expand='version'` answered with `"version": "1.2.3"` out of a package.json at exit 0 with no disclosure (F-R1-02). Two rules, one invariant each: cueOccurrenceIsIntentGate — an intent word is evidence about what the user WANTS; it may never double as the positional evidence that they NAMED something. The cue OCCURRENCE that satisfies the gate is disqualified, never the word, so a later independent cue in the same task still resolves the name. weakEvidenceKind — a weak (all-lowercase, cue-positioned) reading must be backed by a non-Section definition. Identifier-shaped mentions are untouched: there the SHAPE is the evidence. Rank is deliberately not part of the test — k is 0.0000 for nearly every row of any large corpus, so gating on it would make resolution depend on corpus size. WHY THE CORPUS SAID harmful=0.000: bench/taskroute_eval.py::make_repo built a fixture repo whose every symbol was camelCase or Pascal. The weak tier only fires on all-lowercase names, so no row could reach it — the class was invisible by construction. The fixture repo now carries both halves of the collision class (nine lowercase code definitions; a package.json whose keys index as t="sec"), and test/taskroutecheck.sh's repo carries the same. pre-change binary -> post-change binary, same corpus, same day | set | rows | metric | before | after | | audit set A (08-28 shape) | 25 | false recommends | 0 | 0 | | audit set B (word after cue) | 25 | false recommends | 13 | 0 | | prompts.tsv test | 89 | precision/harmful/neg-spec | .797/.135/.657 | 1.000/.000/1.000| | prompts.tsv test | 89 | accuracy/coverage | .787/.870 | .921/.870 | | prompts.tsv dev | 100 | precision/harmful/neg-spec | — | 1.000/.000/1.000| | prompts.tsv dev | 100 | accuracy/coverage | — | .940/.920 | | prompts.tsv all | 189 | precision/harmful | .879/.085 | 1.000/.000 | | 158 pre-existing rows | 158 | (status,intent,resolved) diff | — | 0 differing | RED-FIRST: four new taskroutecheck arms fail against the pre-change binary (each recommended understand-symbol with an --expand), and `bench/taskroute_eval.py --split test` EXITS 1 on the grown corpus (precision under the 0.90 floor, harm over 0.02, specificity under 0.90). The extended fixture repo alone changes nothing: all 158 pre-existing rows are byte-identical on (status, intent, resolved_symbols) across it, and again across the code change. COVERAGE COST, named: exactly one shape is given up — the bare "How does work?" spelling now abstains, and the gate arm for it is inverted into an assertion of the new invariant. The same weak name still routes to --expand through any cue the gate does not consume ("the implementation of classify"), which is what keeps this a rule about self-confirmation rather than a retreat from the weak tier. No corpus row lost its route: every confusion line on both splits is identical to the pre-round run. Map output is untouched — default map, --for, --grep and --pack-task byte-identical between the two binaries; taskroute.h is included by exactly one translation unit. Determinism and xmllint clean. Corpus +31 rows (25 audit-set-B negatives verbatim as evidence, 3 kind-only negatives, 3 positives that hold the recall), seal 25283f2eba85aad889fe3746308df76ed8b1244529f44986c936eb6ef60b0b53, screen flags 1 pre-existing + 1 new (a negative row carrying a live cue phrase — adversarial pressure, not self-quotation), both stated in PROVENANCE.md rather than reworded away. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 1 + bench/taskroute_eval.py | 30 +++++++++++++- docs/EVALS.md | 59 +++++++++++++++++++++++++++ src/taskroute.h | 60 ++++++++++++++++++++++++++- test/taskroutecheck.sh | 47 ++++++++++++++++++++- test/taskroutefix/PROVENANCE.md | 72 +++++++++++++++++++++++++++++++++ test/taskroutefix/prompts.tsv | 31 ++++++++++++++ 7 files changed, 295 insertions(+), 5 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index b3b79a629..9485cebd2 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -876,6 +876,7 @@ ack short-horizon-churn 8433d3adc1682285 5 cid=0d9cec322a8847b5 OPTREMARKS F3 (d ack short-horizon-churn 84e42235c42caa03 4 cid=747524b6a65ad373 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 851e83b4505f10f6 39 cid=bee908e885a04029 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack short-horizon-churn 8535821cb59391a2 5 cid=306ee627ead84e0d 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 85a15278fdb7af3f 7 cid=23b44b4e6a726c33 lane/helptask-precision 2026-09-10 (audit F-R1-01/02): the one gating row is short-horizon-churn churn=self on precededBySymbolCue — the symbol-slot rule was rewritten 2026-08-28 (casing to sentence position) and again 2026-09-02 (flow intents), so editing it inside the same window is thrash by construction and cannot be coded away; this edit is two lines that AND the existing cue test with cueOccurrenceIsIntentGate, and shaping the diff into a pure insertion to score AMBIENT would be gaming the number the legend warns about. The ambient row on resolveTaskSymbols is the three-line kind guard. No other kind moved ack short-horizon-churn 86bb5d2532c4bd1d 13 cid=3e5c5b23b7403422 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 87035166b1f24408 5 cid=3c794a97149e2d62 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 8725e9e3a00708b3 106 W2-F PageRank convergence disclosure: churnRankedGraph carries the RankDisclosure the churn ranking must now disclose (ChurnRanking.pr). churn=self reads THIS wave's own second touch of the function, not new complexity — no branch, no parameter and no symbol was added to it diff --git a/bench/taskroute_eval.py b/bench/taskroute_eval.py index 43d711522..88806a487 100644 --- a/bench/taskroute_eval.py +++ b/bench/taskroute_eval.py @@ -33,10 +33,38 @@ def make_repo(base: Path) -> Path: int renderXmlRow() { return parseConfigValue(); } int sendRequest() { return renderXmlRow(); } int cacheValue() { return sendRequest(); } +int classify() { return cacheValue(); } +int report() { return classify(); } +int patch() { return report(); } +int header() { return patch(); } +int prefix() { return header(); } +int audit() { return prefix(); } +int release() { return audit(); } +int target() { return release(); } +int binary() { return target(); } """, encoding="utf-8", ) - run(["git", "add", "router.cpp"], repo) + # Ordinary English words that are ALSO indexed names are the collision class the weak symbol tier + # draws its false positives from, and until 2026-09-10 this fixture repo had none: every name here + # was camelCase or Pascal, so no corpus row could exercise the tier at all. The nine lowercase + # functions above are the CODE half of the class; the config keys below are the t="sec" half (a JSON + # key or a markdown heading — the kind an English word collides with most often, and the kind + # --expand answers with a line of config rather than a definition). + (repo / "package.json").write_text( + """{ + "name": "route-eval-fixture", + "version": "1.2.3", + "license": "MIT", + "summary": "fixture package for the routing evaluator", + "agent": "ripwire-eval", + "author": "ripwire", + "notes": "keys here index as t=sec symbols, never as code" +} +""", + encoding="utf-8", + ) + run(["git", "add", "router.cpp", "package.json"], repo) run(["git", "commit", "-qm", "base"], repo) return repo diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..66569cd32 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -2205,6 +2205,65 @@ checks into their own `flowTaskChoice` function (mirroring the existing `instrum extraction) and by inlining the small filler-word loop directly rather than introducing a shared helper that collided token-for-token with `weakSymbolCandidate`'s existing shape. +### `--help-task` weak-tier precision: the self-confirming gate and the config-key read (2026-09-10) + +**The defect, in one sentence each.** `does` was a symbol-slot cue AND `how does` is the +`understand-symbol` gate, so *"how does <indexed-word> …?"* minted the very symbol the gate then +required — **13 of 25** adversarial prose prompts recommended `--expand=<English word>` +(audit F-R1-01/04; the Codex `UserPromptSubmit` hook injects that answer into a live session at +`confidence="high"`). And `resolveTaskSymbols` had no kind filter, so **6 of those 13** names existed +only as `t="sec"` rows — JSON keys and markdown headings — and `--expand='version'` answered with +`"version": "1.2.3"` out of a `package.json`, exit 0, no disclosure (F-R1-02). + +**Why the corpus said `harmful=0.000` throughout.** `bench/taskroute_eval.py::make_repo` built a +fixture repo whose every symbol was camelCase or Pascal. The weak tier only fires on all-lowercase +names, so **no corpus row could reach it**: the class was invisible by construction, not by luck. +This is the same shape as the 2026-08-28 round's own finding — a measured precision of 1.000 over a +population that excludes the failure. The fixture repo now carries both halves of the collision +class (nine lowercase code definitions, plus a `package.json` whose keys index as `t="sec"`), and +the 158 pre-existing rows are **byte-identical on (status, intent, resolved_symbols)** across that +fixture change — the new symbols are reachable only from the new rows. + +**The rule that shipped.** An intent word is evidence about what the user WANTS; it may never +double as the positional evidence that they NAMED something. `cueOccurrenceIsIntentGate` +disqualifies exactly the cue OCCURRENCE that satisfies the gate (`understand`/`understanding` +anywhere, `does` when preceded by `how`) — never the word, so a later independent cue in the same +task still resolves the name. Plus `weakEvidenceKind`: a weak reading must be backed by a +non-`Section` definition; an identifier-shaped mention is untouched, because there the SHAPE is the +evidence. Rank is deliberately NOT part of the kind test (`k` is 0.0000 for nearly every row of any +large corpus, so gating on it would make resolution depend on corpus size). + +**Coverage cost, named rather than summarised.** Exactly one shape is given up: the bare +*"How does <lowercase-name> work?"* spelling now abstains, and `test/taskroutecheck.sh`'s arm for it +is inverted into an assertion of the new invariant. The same weak lowercase name still routes to +`--expand` through any cue the gate does not consume (*"the implementation of classify"*), which is +what makes this a rule about self-confirmation rather than a retreat from the weak tier. **No corpus +row lost its route**: every confusion line on both splits is identical to the pre-round run. + +**Measured, pre-change binary → post-change binary, same corpus (189 rows), same day:** + +| Set | Rows | Metric | Before | After | +| --- | ---: | --- | ---: | ---: | +| audit set A (2026-08-28 shape) | 25 | false recommends | 0 | **0** | +| audit set B (word after a cue) | 25 | false recommends | **13** | **0** | +| `prompts.tsv` test | 89 | precision / harmful / neg-spec | 0.797 / 0.135 / 0.657 | **1.000 / 0.000 / 1.000** | +| `prompts.tsv` test | 89 | accuracy / coverage | 0.787 / 0.870 | **0.921** / 0.870 | +| `prompts.tsv` dev | 100 | precision / harmful / neg-spec | — | **1.000 / 0.000 / 1.000** | +| `prompts.tsv` dev | 100 | accuracy / coverage | — | **0.940** / 0.920 | +| `prompts.tsv` all | 189 | precision / harmful | 0.879 / 0.085 | **1.000 / 0.000** | +| 158 pre-existing rows | 158 | (status, intent, resolved_symbols) diff | — | **0 differing rows** | + +The pre-change `split=test` run **exits 1** on the grown corpus (precision under the 0.90 floor, +harm over 0.02, specificity under 0.90), and four `test/taskroutecheck.sh` arms are red against the +pre-change binary — the red-first proof that the corpus and the gate can now see this class. The map +itself is untouched: default map, `--for`, `--grep` and `--pack-task` are byte-identical between the +two binaries on this repo, and `src/taskroute.h` is included by exactly one translation unit. + +**The 2026-08-28 set is not in the repo.** That round's 20 adversarial prompts were never committed +(`git log -S`, whole-tree grep: absent). Set A above — 25 prompts of the same shape, containing that +round's own repro string verbatim — is the stand-in, and it was 0/25 both before and after: the +sentence-POSITION fix that round shipped did not regress; it was defeated by a phrasing it never saw. + ### Skill-routing surface forms — S1b round, PRE-REGISTERED 2026-08-19 (before any skill edit) **Why this round exists, and why it is close to one already rejected.** The S1 round above ran a diff --git a/src/taskroute.h b/src/taskroute.h index 2ad167fa7..575e6a83d 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -150,6 +150,40 @@ inline constexpr std::string_view kWeakSymbolCues[] = { inline constexpr std::size_t kMinWeakSymbolLen = 5; +// A cue occurrence that is ALSO the word satisfying an intent gate is not evidence of a symbol slot. +// Without this the understand-symbol route confirms itself out of thin air: its gate is +// `understand | implementation | how does`, and `does`/`understand` were both symbol-slot cues, so every +// English question of the form `how does …?` minted the very symbol the gate then required +// — the same two words playing both parts (a question about version bumping on a team recommended +// --expand='version', 13 of 25 adversarial prose prompts, 2026-09-10 audit F-R1-01). An intent word is +// evidence about what the user WANTS; it may never double as the positional evidence that they NAMED +// something. Only occurrences are disqualified, never words: a LATER cue in the same task still resolves +// the name (a how-does question that later asks for the body OF the same name routes on that `of`), which is what +// keeps the rule about self-confirmation rather than about the weak tier as a whole. Kept next to the cue +// table, and complete with respect to that gate's three phrases — `implementation` is not a cue at all. +inline bool cueOccurrenceIsIntentGate( std::string_view lowerTask, std::size_t begin, std::string_view cue ) noexcept +{ + if( cue == "understand" || cue == "understanding" ) + { + return true; // the gate reads `has( lower, "understand" )`, which this occurrence already satisfies + } + if( cue != "does" ) + { + return false; + } + std::size_t end = begin; + while( end > 0 && lowerTask[end - 1] == ' ' ) + { + --end; + } + std::size_t from = end; + while( from > 0 && wordByte( lowerTask[from - 1] ) ) + { + --from; + } + return lowerTask.substr( from, end - from ) == "how"; // "how does" IS the gate +} + // True when the word immediately before `pos` is a symbol-slot cue. `lowerTask` is the lowercased task, // so the comparison is a plain equality. Opening quotes and backticks between the cue and the name are // stepped over — they are themselves symbol evidence, never separators. @@ -167,8 +201,26 @@ inline bool precededBySymbolCue( std::string_view lowerTask, std::size_t pos ) n --begin; } const std::string_view word = lowerTask.substr( begin, end - begin ); - return std::any_of( std::begin( kWeakSymbolCues ), std::end( kWeakSymbolCues ), - [word]( const std::string_view cue ) { return cue == word; } ); + if( std::none_of( std::begin( kWeakSymbolCues ), std::end( kWeakSymbolCues ), + [word]( const std::string_view cue ) { return cue == word; } ) ) + { + return false; + } + return !cueOccurrenceIsIntentGate( lowerTask, begin, word ); +} + +// A WEAK reading needs the name to be backed by a CODE definition. A t="sec" row is a markdown heading or +// a JSON/TOML/YAML config key — doc structure and data, isolated in the call graph — and an ordinary +// English word collides with those far more often than with a function: six of the thirteen names the +// weak tier falsely resolved on adversarial prose existed ONLY as t="sec" (`version`, `summary`, +// `license`, `agent`, `author`, `notes` — 2026-09-10 audit F-R1-02), so --expand='version' answered with +// `"version": "1.2.3"` out of a package.json at exit 0. The filter is scoped to the weak tier: an +// identifier-shaped (camel/snake/scoped) mention still resolves whatever kind it names, because there the +// SHAPE is the evidence. Rank is deliberately not part of this test — k is 0.0000 for nearly every row in +// any large corpus, so gating on it would make resolution depend on corpus size. +inline bool weakEvidenceKind( SymKind kind ) noexcept +{ + return kind != SymKind::Section; } // A name with no identifier punctuation and no capital is a WEAK match: it might be a symbol mention, or @@ -238,6 +290,10 @@ inline std::vector resolveTaskSymbols( std::string_view task, const { continue; } + if( !at.strong && !weakEvidenceKind( sym.kind ) ) + { + continue; // this definition is a heading or a config key — no weak evidence (see weakEvidenceKind) + } std::vector& bucket = at.strong ? found : weak; const bool duplicate = std::any_of( bucket.begin(), bucket.end(), [&]( const At& s ) { return s.name == sym.name; } ); if( !duplicate ) diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 8750b2b52..39fe321db 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -24,6 +24,7 @@ int targetSymbol() { return gammaNode(); } int classify() { return targetSymbol(); } int report() { return classify(); } int summary() { return report(); } +int patch() { return summary(); } int computeBudget( int rawBytes ) { int budget = rawBytes / 2; @@ -31,7 +32,19 @@ int computeBudget( int rawBytes ) return budget + reserve; } SRC -git -C "$REPO" add router.cpp +# A config file is part of the fixture on purpose: its keys index as t="sec" symbols with names that are +# ordinary English words, which is the collision class the weak symbol tier draws its false positives from +# (an English word meets a JSON key far more often than a function). Without a t="sec" row in the fixture +# the kind-filter arms below cannot fail, and the class stayed invisible to this gate until 2026-09-10. +cat >"$REPO/package.json" <<'JSON' +{ + "name": "router-fixture", + "version": "1.2.3", + "license": "MIT", + "notes": "fixture package for the router gate" +} +JSON +git -C "$REPO" add router.cpp package.json git -C "$REPO" commit -qm base route(){ "$BIN" "$REPO" --no-cache --help-task="$1" 2>"$TMP/err"; } @@ -59,7 +72,7 @@ case "$EC" in *'status="recommend"'*'intent="edit-contract"'*'--edit-check='*'ta # word"; sentence POSITION is the real discriminator, so these arms assert both directions of it. The two # recall arms are red against a pre-fix binary (both abstained, resolved_symbols="0"); the four precision # arms are the guard that the relaxation did not buy recall with prose false-positives. -LW="$( route 'How does classify work?' )" +LW="$( route 'Explain the implementation of classify' )" case "$LW" in *'status="recommend"'*'intent="understand-symbol"'*'--expand='*'classify'*) ok "lowercase name in an understand slot -> --expand";; *) no "lowercase understand route wrong: $LW";; esac LE="$( route 'I just edited classify; did I change its contract?' )" case "$LE" in *'status="recommend"'*'intent="edit-contract"'*'--edit-check='*'classify'*) ok "lowercase name in a post-edit slot -> --edit-check";; *) no "lowercase edit-contract route wrong: $LE";; esac @@ -75,6 +88,36 @@ case "$LS" in *'--connect='*) no "several lowercase words minted a --connect rou LC="$( route 'how do classify, report and summary connect?' )" case "$LC" in *'--connect='*) no "three lowercase words minted a --connect route: $LC";; *) ok "three lowercase words never satisfy the three-symbol --connect";; esac +# ── the weak tier may not confirm itself, and may not read a config key as code (2026-09-10) ─────────── +# Two independent defects, two independent arms each; all four recommend-side arms are RED against a +# pre-change binary (each recommended understand-symbol with an --expand). +# +# (1) SELF-CONFIRMATION. `does` was a symbol-slot cue AND `how does` is the understand-symbol gate, so +# "how does …?" minted the very symbol the gate then required — the words are the same +# two words. Same for `understand` as cue and `understand` as gate. An intent word is evidence about +# what the user WANTS; it may never double as the positional evidence that they NAMED something. +# Cost, stated plainly: the bare "How does classify work?" spelling no longer routes. That recall is +# reachable through any cue the gate does not itself consume — the LW arm above ("the implementation +# of classify") is that same weak lowercase name, still resolving, still routing to --expand. +# (2) KIND. A t="sec" row is a markdown heading or a JSON/TOML/YAML key. `version` is a config key here +# and in most repos; --expand='version' then answers with `"version": "1.2.3"` at exit 0. A weak +# reading must be backed by a CODE definition; a strong (camel/snake/scoped) mention is untouched. +SC1="$( route 'how does patch Tuesday affect our support load?' )" +case "$SC1" in *'--expand='*) no "the understand gate minted its own symbol out of 'how does': $SC1";; *) ok "'how does ' never mints the symbol its own gate requires";; esac +SC2="$( route 'How does classify work?' )" +case "$SC2" in *'--expand='*) no "self-confirming 'how does' route still fires on a real function: $SC2";; *) ok "'how does ' abstains — the gate word may not be the cue (recall via the slot arm above)";; esac +SC3="$( route 'I want to understand summary writing for the leadership review' )" +case "$SC3" in *'--expand='*) no "the understand gate minted its own symbol out of 'understand': $SC3";; *) ok "'understand ' never mints the symbol its own gate requires";; esac +KF1="$( route 'Explain the implementation of version' )" +case "$KF1" in *'--expand='*) no "a t=sec config key resolved as a weak symbol: $KF1";; *) ok "a config-key-only name never resolves from the weak tier";; esac +# The kind filter is scoped to the WEAK tier: an identifier-shaped mention still resolves whatever it names. +KF2="$( route 'Explain the implementation of targetSymbol' )" +case "$KF2" in *'status="recommend"'*'intent="understand-symbol"'*'--expand='*'targetSymbol'*) ok "an identifier-shaped mention still resolves (kind filter is weak-tier only)";; *) no "kind filter leaked into strong mentions: $KF2";; esac +# And --expand on the config key is the answer the router would have handed over: still a real command, +# just never one the router mints out of prose. (Run it: the honesty is that this is what it returns.) +KF3="$( "$BIN" "$REPO" --no-cache --expand='version' )" +case "$KF3" in *'"version": "1.2.3"'*) ok "the refused route's own command really does answer with a JSON key";; *) no "the kind-filter premise no longer holds: --expand=version returned something else";; esac + # ── paraphrase tolerance: neither intent may recognise only the wording it was written against ───────── # exact-grep and edit-contract shipped as fixed OR-chains of four or five literal phrases. These six are # the realistic paraphrases that missed; all six abstained against a pre-fix binary. The two guard arms diff --git a/test/taskroutefix/PROVENANCE.md b/test/taskroutefix/PROVENANCE.md index 4a3c72cdb..a9d32256b 100644 --- a/test/taskroutefix/PROVENANCE.md +++ b/test/taskroutefix/PROVENANCE.md @@ -157,3 +157,75 @@ resolves" shape and the "wording never scores" shape per intent. **Seal: sha256(prompts.tsv) = `b113a217a19237a1616f81fe412b06475df848e5974214f1efc496db2519dcc0`** (post-round; rows=158, dev=92, test=66). + +## Weak-tier precision round (2026-09-10, lane/helptask-precision) + +**Why the corpus grew.** The 2026-09-10 audit (F-R1-01/02) showed the weak symbol tier recommending +`--expand=` on 13 of 25 adversarial prose prompts, and the committed corpus scoring +`harmful=0.000` throughout — because **the evaluator's fixture repo had no lowercase English-word +symbols at all**. Every name in `make_repo` was camelCase or Pascal, so no row could reach the weak +tier, and the class was invisible by construction. Two things changed together, and neither is +useful without the other: + +- `bench/taskroute_eval.py::make_repo` gained nine lowercase code definitions (`classify`, `report`, + `patch`, `header`, `prefix`, `audit`, `release`, `target`, `binary`) and a `package.json` whose keys + index as `t="sec"` symbols (`version`, `summary`, `license`, `agent`, `author`, `notes`) — the two + halves of the collision class: an English word that IS code, and an English word that is only a + config key. `test/taskroutecheck.sh`'s own fixture repo gained the same two halves (`patch`, plus a + `package.json` carrying `version`/`license`/`notes`). +- **Measured control:** on the 158 pre-existing rows the extended fixture repo changed nothing — + `split=test/dev/all` accuracy, precision, harm, specificity, coverage and every confusion line are + byte-identical before and after the repo grew (same pre-change binary). The new symbols are reachable + only from the new rows. + +**Rows added: 31 (23 test, 8 dev).** Split by the same content-hash rule +(`sha256(prompt)[0] < 0x4D → dev`), computed mechanically per row. + +- **25 negatives, `provenance=handwritten-auditR1`** — the audit's own adversarial set + (`$S/r1/s2b_adversarial.tsv`), quoted verbatim as evidence: non-code questions whose subject word is + also an indexed name, placed directly after a symbol-slot cue. 13 of them recommended before this + round. They are recorded under a `handwritten*` provenance deliberately, so the trigram screen and + the split rule both apply to them. +- **3 negatives, `provenance=instrumented-cli`** — the `t="sec"` half stated in the understand card's + own closed vocabulary (`the implementation of version|license|author`). These are caught ONLY by the + kind filter: their intent word is disjoint from the cue that mints the name, so the + self-confirmation rule never sees them. Same `instrumented-cli` rationale as the 2026-09-02 section + above (a paraphrase that still triggers a closed-phrase intent necessarily reuses a card phrase). +- **3 positives (`understand-symbol`), `provenance=instrumented-cli`** — the recall the fix must NOT + buy its precision with: a lowercase weak name still routing to `--expand` through a cue the gate does + not itself consume (`the implementation of prefix`, `the implementation of audit`), and the sharpest + statement of the invariant — a how-does question that later asks for the body OF the same name, which + routes on that second, independent cue occurrence. + +**Screen result: 2 flagged lines, one pre-existing and one new, both stated rather than reworded.** +`python3 test/taskroutefix/contamination_screen.py --bin build/ripwire`: + +- `line 61, 'i change its'` — the pre-existing `handwritten-digD-10` flag documented in the 2026-09-02 + section above. Unchanged, still out of scope. +- `line 176, 'the value of'` — new, on the negative row *what is the value of module thinking in org + design?*. The trigram collides with the `kVariableSlotCues` literal `"the value of"`. It is not + reworded, for two reasons: the row is audit evidence quoted verbatim, and card vocabulary inside a + NEGATIVE row is adversarial pressure (a live cue phrase that must still not route), the opposite of + the self-quotation the screen exists to catch. The screen makes no positive/negative distinction and + was deliberately not taught one to pass this round. + +`FIXTURE_SYMBOLS` in the screen was deliberately NOT extended with the new lowercase names: they are +ordinary English words, so exempting them would blank real prose out of every screened row and hide +flags the screen is there to raise. + +**Scoring run, same binary, three splits** (`python3 bench/taskroute_eval.py --bin build/ripwire +--corpus test/taskroutefix/prompts.tsv --split …`), pre-change binary → post-change binary: + +| split | rows | accuracy | precision | harmful | neg-specificity | coverage | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| test | 89 | 0.787 → **0.921** | 0.797 → **1.000** | 0.135 → **0.000** | 0.657 → **1.000** | 0.870 → 0.870 | +| dev | 100 | — → **0.940** | — → **1.000** | — → **0.000** | — → **1.000** | — → 0.920 | +| all | 189 | 0.847 → **0.931** | 0.879 → **1.000** | 0.085 → **0.000** | 0.733 → **1.000** | 0.899 → 0.899 | + +The pre-change `split=test` run **exits 1** (precision below the 0.90 floor, harm above 0.02, +specificity below 0.90): the corpus can now fail on this class, which is the whole point of the round. +Coverage is unmoved and every confusion line is identical to the pre-round run — no actionable row lost +its route. + +**Seal: sha256(prompts.tsv) = `25283f2eba85aad889fe3746308df76ed8b1244529f44986c936eb6ef60b0b53`** +(post-round; rows=189, dev=100, test=89). diff --git a/test/taskroutefix/prompts.tsv b/test/taskroutefix/prompts.tsv index f3ac723db..160b6226b 100644 --- a/test/taskroutefix/prompts.tsv +++ b/test/taskroutefix/prompts.tsv @@ -157,3 +157,34 @@ dev clean who-writes handwritten-digE who modifies StorageDriver once the pipeli dev clean who-writes handwritten-digE who sets renderXmlRow before the page ships dev clean abstain handwritten-digE the failure happened somewhere around line 12 but I do not have a filename yet test clean abstain handwritten-digE the dashboard numbers on the nightly run look stale and nobody can say why +test clean abstain handwritten-auditR1 how does version bumping work on this team? +test clean abstain handwritten-auditR1 how does release week usually go for you all? +dev clean abstain handwritten-auditR1 I want to understand budget season better before the offsite +test clean abstain handwritten-auditR1 how does summary writing help the leadership review? +test clean abstain handwritten-auditR1 what is the cost of legend placement on the marketing page? +dev clean abstain handwritten-auditR1 how does license renewal get approved? +dev clean abstain handwritten-auditR1 tell me about the process of owners meeting every month +test clean abstain handwritten-auditR1 how does patch Tuesday affect our support load? +dev clean abstain handwritten-auditR1 the price of release candidates in engineer time is too high +dev clean abstain handwritten-auditR1 how does audit season work at a company this size? +test clean abstain handwritten-auditR1 the concept of baseline expectations for a new hire +dev clean abstain handwritten-auditR1 how does agent onboarding differ from employee onboarding? +test clean abstain handwritten-auditR1 I need to understand author attribution rules for the paper +test clean abstain handwritten-auditR1 the idea of brief writing as a leadership skill +dev clean abstain handwritten-auditR1 how does header design affect the newsletter open rate? +test clean abstain handwritten-auditR1 the notion of active listening in one-on-ones +test clean abstain handwritten-auditR1 what is the value of module thinking in org design? +test clean abstain handwritten-auditR1 how does notes taking work best in a long meeting? +dev clean abstain handwritten-auditR1 the practice of build weeks every quarter +test clean abstain handwritten-auditR1 how does target setting work for the sales team? +test clean abstain handwritten-auditR1 what does result orientation mean for performance reviews? +test clean abstain handwritten-auditR1 the cost of update fatigue among our customers +test clean abstain handwritten-auditR1 how does prefix branding work for product families? +test clean abstain handwritten-auditR1 the role of parent companies in this acquisition +test clean abstain handwritten-auditR1 how does binary thinking hurt a design discussion? +test clean abstain instrumented-cli explain the implementation of version to the new hire +test clean abstain instrumented-cli i need to understand the implementation of license terms for this quarter +test clean abstain instrumented-cli walk me through the implementation of author guidelines for the newsletter +test clean understand-symbol instrumented-cli walk me through the implementation of prefix before i touch it +test clean understand-symbol instrumented-cli i want to understand the implementation of audit end to end +test clean understand-symbol instrumented-cli how does classify work? show me the body of classify From 8bb4d9f31768695395f33b12cb245b73398656c7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:15:52 -0400 Subject: [PATCH 07/73] =?UTF-8?q?fix(limits):=20"every=20compile-time=20ca?= =?UTF-8?q?p=20in=20src/"=20was=20120=20of=20212=20=E2=80=94=20the=20shape?= =?UTF-8?q?=20of=20one=20habit,=20not=20of=20a=20population?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/LIMITS.md opens "Every compile-time cap in `src/`". docs/limits_build.py's DECL required the literal `inline constexpr` with the value on the SAME line, and KEY named a cap by keyword. Between them, 92 declarations under 81 distinct names were invisible to a register every cap round has been run against. `inline` is optional at namespace scope and FORBIDDEN on a class member, so "inline constexpr" was never the shape of the population. census before after cap declarations parsed 120 212 distinct names 119 200 caps (truncating) 114 205 ranking parameters 6 7 classified in the sidecar 26 108 What was outside it, with the verb each reaches: kType3MaxBucket / kType3MaxTokensForLcs src/clones.h bound clone DETECTION — clone_groups and dup_pct are floors, and nothing said so kSkillScanFindingCap src/skillscan.h bounds a SECURITY verdict (--scan-skills) kHandoffSymbolsPerFile src/handoff.h truncates output and DISCLOSES syms_capped kMaxFlipRows / kMaxNearMisses src/flipimpact.h a file that emits no disclosure at all kMaxSitesShown src/darkflags.h likewise kChaConeCap src/graph.h the CHA cone memo behind --grep kMaxAnchorsShown src/docdrift.h --doc-drift anchors kRenameMaxPairs / kRenameMaxChain src/quality.h --quality-delta's rename-ack window kSituBlastFilesShown src/situ.h the "showing 8 of 69 files" in --situ DECL now accepts `(?:static\s+)?(?:inline\s+)?constexpr` and a wrapped initializer (the scan moved to the file text with re.M); KEY gained `Shown|PerFile|Hits`, which is what made kHandoffSymbolsPerFile invisible to this register AND to docs/TUNING.md simultaneously. A THIRD CLASS, named in review on #108. `kUnitSizeLowRiskMax = 15` decides which SIDE of a rule a unit falls on; `kMaxNameLen = 96` decides that a 97-character backticked token is a sentence, not an identifier; `kMaxPartitions = 16` bounds a hand-written `--partition=N`. None truncates anything, so none can be judged by shown/total and none should ever emit `capped="1"` — labelling them OUTPUT would ask for a disclosure that could never honestly fire. BOUNDARY is now a value docs/limits_classes.tsv accepts, and 25 rows carry it. 82 new rows were classified by reading the call site, not the name: 27 INDEXING, 24 OUTPUT, 25 BOUNDARY (plus 6 pre-existing rows re-tagged BOUNDARY). Nine constants I could not classify honestly render `—`, which the document defines as NOT YET CLASSIFIED, never "neither": kMaxGitWorkers is a worker count, kCapPerThread a batch size, kCap a buffer. `kMaxCacheBlobAgeDays` (30.0) and `kRadixThreshold` would have landed in the "ranking and apportionment parameters" table — the first because it is fractional, the second because its name says Threshold — and each would have rendered **unsourced**, which is a false claim about both. Thirty days is not a proportion and a radix cutover apportions nothing. NOT_A_WEIGHT keeps them in the cap table as BOUNDARY. GATE. New arm (H) in test/limitstablecheck.sh plants a plain `constexpr`, a `static constexpr` member and a wrapped initializer — each alone in a synthetic --root tree, each required to appear, with a non-cap constant beside it required NOT to — plus a control that the NAME filter admits `*PerFile`. Red-proven by reverting the generator: mutation result DECL back to `inline constexpr`, same line (H) plain/static/wrapped all red + (B) 68 sidecar rows name a cap the register can no longer see KEY without Shown|PerFile|Hits (H) control red + (B) 7 sidecar rows orphaned Arm (G) reads the class cell back out of the rendered markdown and its vocabulary now includes BOUNDARY, so a class cannot be right in the sidecar and wrong on the page. docs/EVALS.md carried "the 114 caps in src/ … the 120 cap-shaped constants": corrected, with the recipe and the reason the old number was wrong, because a count published without its recipe is one list counted four defensible ways. --- bench/capsweep/capsweep.py | 2 +- docs/EVALS.md | 13 +- docs/LIMITS.md | 342 +++++++++++++++++++++++++++++++++++-- docs/limits_build.py | 63 +++++-- docs/limits_classes.tsv | 88 +++++++++- test/limitstablecheck.sh | 60 ++++++- 6 files changed, 527 insertions(+), 41 deletions(-) diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index f9adaed28..8d4a8a596 100644 --- a/bench/capsweep/capsweep.py +++ b/bench/capsweep/capsweep.py @@ -14,7 +14,7 @@ prepare copy + patch + build the tunable binary (caps that must stay constexpr are detected by compiling and excluded, iteratively, so a cap used as an array bound cannot break the sweep) screen corpus at BASELINE vs ALL-CAPS-BUMPED -> the commands that are cap-sensitive at all. - This is the step that makes it tractable: 120 caps x 195 commands is 23,400 runs, but most + This is the step that makes it tractable: ~120 caps x 195 commands is 23,400 runs, but most commands respond to no cap, so the second phase only pays for the ones that move. sweep per cap x sensitive command, a value ladder -> bytes at each value emit markdown tables -> docs/TUNING.md (--check compares instead of writing; the gate runs that) diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b1058118..d268b8e83 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -5166,9 +5166,16 @@ names, while its stated cost — "~3.5 KB per `--pack-task` bundle" — was not `--pack-task` emits no `sibs=` at all, before or after. Symbols-per-file here is median 4, p90 18, p99 85; 100 clears the tail, fires on 15.8% of bodies, costs +36% on a single-symbol `--expand` answer and **nothing** on `--for` or `--pack-task`, which are byte-identical at every cap. The full inventory -of the 114 caps in `src/` — and of the 6 ranking parameters partitioned out of the same census, which -together make the 120 cap-shaped constants the generator parses — is `docs/LIMITS.md`, generated and -gated by `test/limitstablecheck.sh`. What each cap COSTS, measured per verb, is `docs/TUNING.md`. +of the 205 caps in `src/` — and of the 7 ranking parameters partitioned out of the same census, which +together make the 212 cap-shaped constants the generator parses — is `docs/LIMITS.md`, generated and +gated by `test/limitstablecheck.sh`. Those figures were **114 / 6 / 120** until 2026-09-10, and the +difference is not new code: `docs/limits_build.py` required the literal `inline constexpr` with the +value on the same line, so 92 declarations under 81 distinct names — `kType3MaxBucket`, which bounds +clone DETECTION; `kSkillScanFindingCap`, which bounds a security verdict; `kHandoffSymbolsPerFile`, +which truncates output and discloses — were outside a register whose first line says "Every +compile-time cap in `src/`". The old number was the size of a regex's output presented as the size of a +population; `test/limitstablecheck.sh` arm (H) now plants a plain `constexpr`, a `static constexpr` +member and a wrapped initializer in a synthetic tree and requires each to appear. What each cap COSTS, measured per verb, is `docs/TUNING.md`. The three figures moved together on 2026-08-15, and the cause is on the *denominator* side, not this verb's: `--expand`'s `` bodies now carry `sibs=`/`inc=` file-context attributes, which grows the diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 237a4eeb5..d2163a7dd 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -10,12 +10,12 @@ where the pathological tail is, never near the typical case — and when it fire | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 114 | 50 | 79 | **35** | +| 205 | 81 | 99 | **106** | -Plus 6 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 114 + 6 is the 120 constants this generator parses out of `src/`. +Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they +are not counted as caps, and 205 + 7 is the 212 constants this generator parses out of `src/`. -## INDEXING or OUTPUT — which half of the answer a cap bounds +## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds **INDEXING** caps bound what can EVER be found. A silent one is unrecoverable by the caller: no flag, no budget, no second call gets the answer back, and the output reads as "none exists". @@ -23,9 +23,18 @@ flag, no budget, no second call gets the answer back, and the output reads as "n `--detail`, a page or a follow-up call can recover the answer. The two are not the same severity and a single table that does not distinguish them invites fixing the cheap one first. -The `class` column below carries that answer where it is known. **26 of 114 caps are classified -(10 INDEXING, 16 OUTPUT); the remaining 88 render `—`, which means NOT YET CLASSIFIED — never -"neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: +**BOUNDARY** is the third answer and it is not a cap at all — it is the one the census kept +getting wrong. `kUnitSizeLowRiskMax = 15` decides which SIDE of a rule a unit falls on ("15 lines +or fewer is low-risk"); `kMaxNameLen = 96` decides that a 97-character backticked token is a +sentence rather than an identifier; `kMaxPartitions = 16` bounds a hand-written `--partition=N`. +None of them truncates anything, so none can be judged by `shown=`/`total=` and none should carry +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. **108 of 205 caps are classified +(37 INDEXING, 36 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 produced the taxonomy could not touch `src/`. `test/limitstablecheck.sh` fails if a row there names a cap that no longer exists. @@ -55,7 +64,7 @@ measurement that chose it; listing them beside truncation caps invites tuning th The **anchor** column is read from each constant's own trailing comment. **unsourced** means the comment cites no measurement — the value came from somewhere, but not from anything a reader can -check. All 6 read unsourced today, which is the finding, not an omission: `kSpecificMinLen` has +check. All 7 read unsourced today, which is the finding, not an omission: `kSpecificMinLen` has the widest measured blast radius of any constant in this tree (14 invocations across 9 verbs, per `docs/TUNING.md`) and its entire stated provenance is the parenthetical `(aider's)`. Sourcing them means editing `src/`; a cited anchor that `docs/EVALS.md` does not contain makes this generator @@ -67,9 +76,18 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. | `kCeilingFirstEntryTolerance` | `1.15` | `src/serialize.h:616` | **unsourced** | — | | `kCommonNameDefThreshold` | `5` | `src/graph.h:244` | **unsourced** | >5 defs of the same name ⇒ common (aider's) | | `kCoreBudgetShare` | `0.34` | `src/partition.h:98` | **unsourced** | — | +| `kExemplarCcxCeilFactor` | `4` | `src/exemplar.h:58` | **unsourced** | — | | `kSpecificMinLen` | `8` | `src/graph.h:250` | **unsourced** | ≥8 chars … (aider's) | | `kZoneDistanceThreshold` | `0.5` | `src/arch.h:742` | **unsourced** | \|A+I-1\| past this → classify into pain/useless | +### `src/abicheck.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxStructsPerRef` | `12` | 129 | OUTPUT | display cap per ref (mirrors crossref::kStrayFilesPerRef); --detail lifts it | + ### `src/accessshape.h` Discloses: `loops_capped` @@ -113,6 +131,25 @@ Discloses: `bridges_capped`, `files_capped`, `inc_capped`, `modules_capped`, `ro | `kIntFlagMax` | `1000000000` | 3092 | — | parsePosInt/parseNonNegInt's own overflow ceiling | | `kPageValueMax` | `1000000000` | 591 | — | — | +### `src/cloneidiom.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kIdiomMaxCondTokens` | `8` | 81 | BOUNDARY | `( a.b < Limit::Hi )` is 7; anything longer is not a scalar threshold | +| `kIdiomMaxLabelTokens` | `6` | 82 | BOUNDARY | `case Enum::Member :` | +| `kIdiomMaxReturnTokens` | `6` | 80 | BOUNDARY | `return Enum::Member ;` is 3; a call or an expression is not a table return | + +### `src/clones.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kType3MaxBucket` | `1024` | 475 | INDEXING | skip fingerprint buckets larger than this (stop-gram cut) | +| `kType3MaxTokensForLcs` | `4096` | 456 | INDEXING | cap the LCS DP dimension per body (cost guard) | + ### `src/commentcoherence.h` Discloses: **none** @@ -131,15 +168,68 @@ Discloses: `defs_capped`, `files_capped`, `syms_capped` | `kFileRowCap` | `40` | 89 | — | — | | `kSymbolRowCap` | `40` | 88 | — | — | +### `src/crossref.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxGitWorkers` | `12` | 802 | — | matches the ingest pool's measured ~12-way; these are | +| `kMaxRefs` | `512` | 130 | INDEXING | refusal bound — a sweep, not a fork-network crawl | +| `kWhereisHits` | `60` | 135 | OUTPUT | — | + +### `src/darkflags.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxAliasDepth` | `8` | 859 | INDEXING | — | +| `kMaxEnvNameLen` | `128` | 58 | BOUNDARY | longest plausible environment-variable name | +| `kMaxSitesShown` | `8` | 57 | OUTPUT | per gate, per list; the rest are counted in a | + +### `src/didyoumean.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxEditDistance` | `3` | 155 | BOUNDARY | same bandwidth cutoff as didYouMean | +| `kMaxEditDistance` | `3` | 205 | BOUNDARY | bandwidth cutoff (§P12.1): beyond this a "hint" is noise, not help | + ### `src/dmm.h` Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kUnitComplexityLowRiskMax` | `5` | 91 | OUTPUT | cyclomatic complexity | -| `kUnitInterfacingLowRiskMax` | `2` | 92 | OUTPUT | parameters | -| `kUnitSizeLowRiskMax` | `15` | 90 | OUTPUT | lines | +| `kUnitComplexityLowRiskMax` | `5` | 91 | BOUNDARY | cyclomatic complexity | +| `kUnitInterfacingLowRiskMax` | `2` | 92 | BOUNDARY | parameters | +| `kUnitSizeLowRiskMax` | `15` | 90 | BOUNDARY | lines | + +### `src/docdrift.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxAnchorsShown` | `12` | 130 | OUTPUT | drifted anchors printed per doc; detail lifts the cap | +| `kMaxClaimedLine` | `200000` | 138 | BOUNDARY | past this a "line number" is a hostile-input example, not a claim | +| `kMaxDecDigits` | `10` | 134 | BOUNDARY | overflow guard on a doc/code integer literal | +| `kMaxExtLen` | `6` | 136 | BOUNDARY | "cpp", "swift", "metal" — longer is not an extension | +| `kMaxFrontMatter` | `12` | 150 | — | — | +| `kMaxHexDigits` | `15` | 135 | BOUNDARY | …hex fits 15 nibbles in 64 bits with room to spare | +| `kMaxNameLen` | `96` | 133 | BOUNDARY | past this it is a sentence, not an identifier | +| `kMinMentionLen` | `4` | 131 | BOUNDARY | a backticked name shorter than this is prose, not code | +| `kMinValueNameLen` | `3` | 132 | BOUNDARY | …and the bar for a `= N` / `[N]` subject name | + +### `src/editcheck.h` + +Discloses: `unflagged_capped` + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kEditCheckSpellingsShown` | `6` | 154 | OUTPUT | — | ### `src/editpreview.h` @@ -159,6 +249,15 @@ Discloses: `files_capped`, `findings_capped`, `syms_capped` | `kEnsembleSymbolRowCap` | `40` | 107 | — | — | | `kOrdinalWindowCap` | `40` | 112 | — | — | +### `src/eval.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxSample` | `80` | 275 | INDEXING | — | +| `kMaxScored` | `4000` | 697 | INDEXING | — | + ### `src/expand.h` Discloses: **none** @@ -168,6 +267,19 @@ Discloses: **none** | `kExpandMaxPer` | `8` | 37 | OUTPUT | — | | `kExpandMaxSeeds` | `8` | 36 | OUTPUT | out-of-range env means OFF, never a clamp-and-guess | +### `src/fieldaffinity.h` + +Discloses: `aggs_capped`, `as_loops_capped`, `as_query_capped` + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxAggsModeled` | `8000` | 145 | INDEXING | refusal bound on the whole-repo modelling pass | +| `kMaxFieldsShown` | `32` | 144 | OUTPUT | per struct (touched fields only) | +| `kMaxFnsShown` | `8` | 143 | OUTPUT | per struct | +| `kMaxPairsShown` | `12` | 142 | OUTPUT | per struct | +| `kMaxScopeChars` | `120` | 146 | OUTPUT | displayed prefix of a PROFILE_SCOPE description | +| `kMaxStructsShown` | `20` | 141 | OUTPUT | whole-repo form: the ranked head, `capped="1"` past it | + ### `src/filepool.h` Discloses: **none** @@ -176,6 +288,18 @@ Discloses: **none** | --- | --- | --- | --- | --- | | `kPoolMaxTopK` | `32` | 29 | — | env values outside range mean OFF, never a clamp-and-guess | +### `src/flipimpact.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxBindings` | `32` | 93 | INDEXING | value-style constants tracked — bounds pass B's needle count | +| `kMaxChainDepth` | `8` | 92 | INDEXING | alias-chain depth cap (mirrors darkflags::kMaxAliasDepth) | +| `kMaxFamily` | `64` | 91 | INDEXING | gates one flip may light — an alias fan-out past this is a table, not a switch | +| `kMaxFlipRows` | `25` | 94 | OUTPUT | per emitted list; --detail lifts every cap | +| `kMaxNearMisses` | `5` | 95 | OUTPUT | "did you mean" suggestions on an unknown gate name | + ### `src/gitmine.h` Discloses: `coboost_commits_capped`, `coboost_partners_capped` @@ -186,16 +310,29 @@ Discloses: `coboost_commits_capped`, `coboost_partners_capped` | `kCoBoostMaxPartnerFiles` | `8` | 2839 | INDEXING | strongest partners only, by (deg desc, path asc) | | `kCoBoostMaxSymbolsPerFile` | `3` | 2840 | INDEXING | per partner file: its top-3 symbols by (lens score desc, id asc) | +### `src/gitoracle.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxNameLen` | `96` | 96 | BOUNDARY | past this it is a minified blob, not an identifier | +| `kMaxNamesTracked` | `2000000` | 99 | INDEXING | map bound; 44,904 on the deepest repo measured | +| `kMaxProbeCommits` | `40000` | 97 | INDEXING | walk bound — past it, misses are "unknown", never "never" | +| `kMinNameLen` | `4` | 95 | BOUNDARY | — | + ### `src/graph.h` Discloses: `importers_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | +| `kChaConeCap` | `4096` | 465 | INDEXING | per-walk discovery cap, unchanged from the per-call walk | | `kMaxEdges` | `256` | 5385 | — | total emitted edge cap | | `kMaxNodes` | `96` | 5384 | — | total emitted node cap (§3 size caps) | | `kMaxRadius` | `12` | 5387 | — | — | | `kMaxTerminals` | `16` | 5383 | — | >16 is the CALLER's usage error; the core CLAMPS (never VERIFYs on hostile input) | +| `kMemberSpellingsShown` | `6` | 4325 | OUTPUT | — | ### `src/handoff.h` @@ -206,6 +343,7 @@ Discloses: `syms_capped` | `kHandoffCochangeRows` | `8` | 43 | OUTPUT | heuristic co-change rows shown | | `kHandoffDocRows` | `4` | 41 | OUTPUT | heuristic doc pointers shown | | `kHandoffNoteRows` | `8` | 42 | OUTPUT | heuristic note rows shown | +| `kHandoffSymbolsPerFile` | `6` | 50 | OUTPUT | — | ### `src/infra/blanktext.h` @@ -223,6 +361,16 @@ Discloses: **none** | --- | --- | --- | --- | --- | | `kMaxEvents` | `8` | 62 | — | — | +### `src/infra/sortutil.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kRadixThreshold` | `128` | 194 | BOUNDARY | — | +| `kRadixThreshold` | `2048` | 99 | BOUNDARY | — | +| `kRadixThreshold` | `2048` | 224 | BOUNDARY | — | + ### `src/ingest.h` Discloses: `ellipsis_capped`, `hits_capped` @@ -230,8 +378,67 @@ Discloses: `ellipsis_capped`, `hits_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | | `kBinarySniffCap` | `4096` | 207 | — | NUL-byte sniff window | +| `kMaxSkipRowsPerClass` | `500` | 125 | OUTPUT | — | | `kUnreachableMaxHits` | `5000` | 414 | — | — | +### `src/ingest_astquery.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxEditDistance` | `3` | 377 | BOUNDARY | same bandwidth as didYouMean()'s symbol-name cutoff | + +### `src/ingest_model.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kRadixThreshold` | `64` | 465 | BOUNDARY | — | + +### `src/ingest_names.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxQualifierHops` | `32` | 219 | INDEXING | `a::b::c::…` past 32 segments is not written C++ | + +### `src/ingest_parsepool.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kCapPerThread` | `256` | 92 | — | — | +| `kMaxPendingParsedFiles` | `4` | 286 | — | — | + +### `src/ingest_relations.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxImportContainerDepth` | `256` | 1547 | INDEXING | — | + +### `src/ingest_sidecap.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kSideDepthStd` | `256` | 1150 | INDEXING | FFI / routes / bindings — their own guard | +| `kSideDepthUses` | `512` | 1151 | INDEXING | value-uses — twice the others, as it always was | + +### `src/landingplan.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxPlanScout` | `12` | 68 | OUTPUT | — | + ### `src/lanes.h` Discloses: `blast_capped`, `tests_capped` @@ -241,6 +448,17 @@ Discloses: `blast_capped`, `tests_capped` | `kMaxBlastFiles` | `40` | 120 | — | blast-radius file rows per lane; total + capped always reported | | `kMaxTestRows` | `40` | 121 | — | tests_to_run rows per lane; same "never drop without a number" | +### `src/layout.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxAssertChars` | `220` | 85 | OUTPUT | the displayed prefix of a static_assert's text | +| `kMaxDefsShown` | `24` | 87 | BOUNDARY | a name defined more often than this is a generic, not a mirror | +| `kMaxMacroDepth` | `4` | 83 | INDEXING | object-like macro expansion depth for a type name | +| `kMaxNestDepth` | `8` | 82 | INDEXING | nested-aggregate resolution depth (a cycle stops here) | + ### `src/lexical.h` Discloses: **none** @@ -248,6 +466,16 @@ Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | | `kMaxAnchorDefs` | `3` | 1787 | — | — | +| `kMaxIdentifierLookupWords` | `2` | 1936 | — | — | +| `kMaxShown` | `4` | 1608 | OUTPUT | — | + +### `src/lintcatalog.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxEditDistance` | `3` | 341 | BOUNDARY | — | ### `src/lintrules.h` @@ -271,6 +499,7 @@ Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | +| `kMaxEditDistance` | `3` | 206 | BOUNDARY | the read verbs' bandwidth (didyoumean.h::didYouMean) | | `kReceiptRegionBudgetBytes` | `2048` | 891 | — | — | ### `src/mcpjson.h` @@ -287,6 +516,8 @@ Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | +| `kMaxEditDistance` | `3` | 926 | BOUNDARY | — | +| `kMaxEditDistance` | `3` | 1116 | BOUNDARY | same bandwidth cutoff nearestName searches within | | `kMcpEchoMaxBytes` | `160` | 363 | — | — | ### `src/mcpverbs.h` @@ -298,6 +529,8 @@ Discloses: `coboost_commits_capped`, `hits_capped`, `unindexed_candidates_capped | `kBatchCap` | `16` | 4218 | — | max sub-queries processed per batch; excess is REPORTED, never silently dropped | | `kMcpPageValueMax` | `1000000000` | 306 | — | == cli.h's kPageValueMax | | `kMcpRecallTopKMax` | `1000` | 312 | — | — | +| `kOtherDefCap` | `4` | 3953 | OUTPUT | disclosure, not a listing — cap the tail | +| `kRowCap` | `100` | 889 | — | — | ### `src/mention.h` @@ -362,6 +595,7 @@ Discloses: `mention_syms_capped`, `ranking_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | +| `kOverCeilingKeyBytes` | `22` | 1653 | — | `,"over_ceiling":true` + the closing brace | | `kPackTaskRankTopN` | `12` | 89 | — | ranking = the top-12 head, not the full 40 — leaves budget for the later sections | ### `src/pageview.h` @@ -385,7 +619,7 @@ Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kMaxPartitions` | `16` | 94 | OUTPUT | — | +| `kMaxPartitions` | `16` | 94 | BOUNDARY | — | ### `src/pattern.h` @@ -405,6 +639,18 @@ Discloses: **none** | --- | --- | --- | --- | --- | | `kPrDefaultBudgetTokens` | `8000` | 452 | — | — | +### `src/quality.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxCacheBlobAgeDays` | `30.0` | 1850 | BOUNDARY | — | +| `kMaxCacheBlobCount` | `4096` | 1852 | BOUNDARY | bound every future hygiene scan | +| `kMaxEditLockAgeDays` | `1.0` | 1862 | BOUNDARY | — | +| `kRenameMaxChain` | `8` | 1038 | INDEXING | a→b→c… chain depth followed from one current path (disclosed) | +| `kRenameMaxPairs` | `4000` | 1037 | INDEXING | hard cap on recorded pairs (disclosed when hit) | + ### `src/qualitypanel.h` Discloses: `findings_capped` @@ -437,6 +683,27 @@ Discloses: **none** | --- | --- | --- | --- | --- | | `kGenericMinRunLength` | `32` | 296 | — | — | +### `src/renamemine.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMaxCandidates` | `200000` | 105 | INDEXING | vote-map bound; 560 on the deepest history measured | +| `kMaxHunkSide` | `24` | 103 | INDEXING | per-side cap on the O(n²) line pairing; over-wide hunks are dropped + counted | +| `kMaxIdentLen` | `96` | 102 | BOUNDARY | past this it is a minified blob, not an identifier | +| `kMaxIdentsPerLine` | `256` | 106 | INDEXING | a line with more tokens than this is not hand-written code | +| `kMaxLineLen` | `2000` | 104 | BOUNDARY | a line this long is generated/vendored, not a rename site | +| `kMinIdentLen` | `2` | 101 | BOUNDARY | — | + +### `src/resolve.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kFieldWalkCap` | `16` | 2312 | INDEXING | total visited names — bounds depth and width together | + ### `src/search.h` Discloses: `hits_capped` @@ -450,12 +717,21 @@ Discloses: `hits_capped` | `kMaxExactLen` | `24` | 194 | — | beyond this exact-string length, give up exactness (⊤) | | `kMaxExactSet` | `8` | 193 | — | beyond this many exact strings, give up exactness (⊤) | +### `src/selectorrefuse.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kSelectorFilesShown` | `6` | 42 | OUTPUT | — | + ### `src/serialize.h` Discloses: `calls_capped`, `inc_capped`, `sibs_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | +| `kCap` | `65536` | 420 | — | — | | `kForAnchorBodyBudgetBytes` | `22800` | 794 | — | — | | `kForAutoBodyBudgetBytes` | `6000` | 760 | — | — | | `kForCapTailSigBytes` | `96` | 727 | — | — | @@ -465,6 +741,7 @@ Discloses: `calls_capped`, `inc_capped`, `sibs_capped` | `kForPayloadBudgetBytes` | `7500` | 726 | — | — | | `kMaxExpandIncludes` | `24` | 4584 | — | inc= cap | | `kMaxExpandSibs` | `100` | 4575 | — | sibs= cap — a BLOW-UP GUARD, set above the tail, not a trim of the | +| `kMaxSig` | `240` | 2707 | OUTPUT | — | | `kWithGraphNodeCap` | `8` | 5642 | — | — | ### `src/siblift.h` @@ -483,10 +760,19 @@ Discloses: `tests_capped`, `untested_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | | `kMaxUntestedRows` | `25` | 939 | — | — | +| `kSituBlastFilesShown` | `8` | 348 | OUTPUT | section [1] — blast-radius file rows | | `kSituPartnerFileRowsShown` | `4` | 351 | — | section [1] — decl/def partner rows | | `kSituPartnerRowsShown` | `8` | 350 | — | section [3] — co-change partner rows | | `kSituTestRowsShown` | `25` | 349 | — | section [2] — tests-to-run rows | +### `src/skillscan.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kSkillScanFindingCap` | `200` | 843 | INDEXING | generous for one file or a small dir; caps a pathological --scan-skills sweep | + ### `src/slice.h` Discloses: **none** @@ -534,3 +820,35 @@ Discloses: `seed_files_capped` | --- | --- | --- | --- | --- | | `kRunTraceRelevantLinesCap` | `40` | 696 | — | cap (first/last half split past it) | +### `src/verbs_doctor.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kShown` | `8` | 829 | OUTPUT | — | + +### `src/verbs_for.h` + +Discloses: `coboost_commits_capped` + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kJsonEnvelopeDigitsMax` | `10` | 911 | — | — | + +### `src/verbs_lint.h` + +Discloses: `count_capped`, `ellipsis_capped`, `findings_capped`, `hits_capped`, `rows_capped` + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kMatchMaxHits` | `5000` | 1296 | INDEXING | astQuery's per-spec budget, named not implied | + +### `src/verbs_navigate.h` + +Discloses: `importers_capped` + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kEvidenceCap` | `20` | 1339 | OUTPUT | — | + diff --git a/docs/limits_build.py b/docs/limits_build.py index bb618cd7e..03b22a2e9 100644 --- a/docs/limits_build.py +++ b/docs/limits_build.py @@ -2,7 +2,7 @@ # limits_build.py — generate docs/LIMITS.md: every compile-time cap in src/, what it bounds, and # whether the file it lives in DISCLOSES a truncation when it fires. # -# WHY GENERATED. A hand-kept table of 120 constants is a table that rots. The 2026-09-09 round found a +# WHY GENERATED. A hand-kept table of two hundred constants is a table that rots. The 2026-09-09 round found a # published figure that had been wrong for four days because one number lived in six places and only # four were wired together; the fix is to make the doc a build product with a gate, not a promise. # @@ -16,8 +16,27 @@ import re, sys, pathlib, collections, argparse ROOT = pathlib.Path(__file__).resolve().parent.parent -DECL = re.compile(r'^\s*inline\s+constexpr\s+[\w:<>, ]*?\b(k[A-Z][A-Za-z0-9_]*)\s*=\s*([0-9][0-9_.eE+-]*)\s*;(.*)$') -KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width') +# WHAT THIS REGEX ADMITS, AND WHY IT WAS WIDENED. It used to require the literal `inline constexpr` +# with the value on the SAME line, and to name a cap by a keyword list. Both halves were leaking: +# +# * `inline` is optional at namespace scope and forbidden on a class member, so `constexpr` and +# `static constexpr` declarations were invisible. 92 declarations — 81 distinct names — sat outside +# a register whose own first line says "Every compile-time cap in src/". Among them +# `kType3MaxBucket` (bounds clone DETECTION, so clone_groups is a floor), `kSkillScanFindingCap` +# (bounds a SECURITY verdict), `kMaxFlipRows`/`kMaxSitesShown` (whose files emit no disclosure +# vocabulary at all) and `kChaConeCap`. +# * the value may be wrapped onto the next line, so the scan is over the file text with re.M rather +# than line by line. +# * `Shown|PerFile|Hits` were missing from the NAME filter, which is how `kHandoffSymbolsPerFile` — +# a cap that truncates output and discloses `syms_capped="1"` — appeared in neither this register +# nor docs/TUNING.md, and was raised on 2026-09-10 without ever having been listed anywhere. +# +# The published "120 of 202" was the size of a regex's output presented as the size of a population. +# It is now 212 declarations under 200 names, and the recipe is this comment. +DECL = re.compile(r'^[ \t]*(?:static[ \t]+)?(?:inline[ \t]+)?constexpr[ \t]+[\w:<>, ]*?' + r'\b(k[A-Z][A-Za-z0-9_]*)[ \t]*=[ \t]*(?:\r?\n[ \t]*)?([0-9][0-9_.eE+-]*)[ \t]*;(.*)$', + re.M) +KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width|Shown|PerFile|Hits') # A CAP answers "how many of X survive". A HYPERPARAMETER answers "how is X weighted or apportioned". # They are not the same instrument and must not share a table: a cap is judged by what it truncates and @@ -40,7 +59,15 @@ # review notices it; the gate below reports the partition sizes so an empty guard cannot hide. WEIGHT_NAME = re.compile(r'Mul(?!ti)|Blend|Share|Tolerance|Headroom|Decay|Weight|Prior|Factor|Ratio' r'|Threshold|Min(?:Len|Words|Chars)') +# Two names the widened census brought in that WEIGHT_NAME would misfile. A duration is not a weight — +# 30.0 is thirty days, not a proportion — and `kRadixThreshold` is the point at which a sort switches +# algorithm, which apportions nothing. Both would have rendered in the parameter table as **unsourced** +# ranking parameters, which is a claim about them that is simply false. They are BOUNDARY caps below. +NOT_A_WEIGHT = re.compile(r'AgeDays|RadixThreshold') + def is_weight(name, val): + if NOT_A_WEIGHT.search(name): + return False if WEIGHT_NAME.search(name): return True try: @@ -57,12 +84,11 @@ def scan(): txt = p.read_text(errors='replace') for a in re.findall(r'([a-z_]+)_capped', txt): disc[rel].add(a) - for i, line in enumerate(txt.splitlines(), 1): - m = DECL.match(line) - if not m or not KEY.search(m.group(1)): + for m in DECL.finditer(txt): + if not KEY.search(m.group(1)): continue note = m.group(3).strip().lstrip('/ ').strip() - caps.append((m.group(1), m.group(2), rel, i, note)) + caps.append((m.group(1), m.group(2), rel, txt[:m.start()].count('\n') + 1, note)) return caps, disc def partition(caps): @@ -104,8 +130,8 @@ def read_classes(path): if not line.strip() or line.lstrip().startswith('#'): continue parts = line.split('\t') - if len(parts) != 2 or parts[1].strip() not in ('INDEXING', 'OUTPUT'): - sys.exit('limits_build: %s:%d is not "\\t(INDEXING|OUTPUT)": %r' % (path, i, line)) + if len(parts) != 2 or parts[1].strip() not in ('INDEXING', 'OUTPUT', 'BOUNDARY'): + sys.exit('limits_build: %s:%d is not "\\t(INDEXING|OUTPUT|BOUNDARY)": %r' % (path, i, line)) out[parts[0].strip()] = parts[1].strip() return out @@ -137,17 +163,26 @@ def render(caps, disc, classes): % (len(caps), len(weights), len(allcaps))) ix = sum(1 for c in caps if classes.get(c[0]) == 'INDEXING') op = sum(1 for c in caps if classes.get(c[0]) == 'OUTPUT') - w('## INDEXING or OUTPUT — which half of the answer a cap bounds\n') + bd = sum(1 for c in caps if classes.get(c[0]) == 'BOUNDARY') + w('## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds\n') w('**INDEXING** caps bound what can EVER be found. A silent one is unrecoverable by the caller: no') w('flag, no budget, no second call gets the answer back, and the output reads as "none exists".') w('**OUTPUT** caps bound what is SHOWN from what was found; a silent one is still a defect, but a') w('`--detail`, a page or a follow-up call can recover the answer. The two are not the same severity') w('and a single table that does not distinguish them invites fixing the cheap one first.\n') + w('**BOUNDARY** is the third answer and it is not a cap at all — it is the one the census kept') + w('getting wrong. `kUnitSizeLowRiskMax = 15` decides which SIDE of a rule a unit falls on ("15 lines') + w('or fewer is low-risk"); `kMaxNameLen = 96` decides that a 97-character backticked token is a') + w('sentence rather than an identifier; `kMaxPartitions = 16` bounds a hand-written `--partition=N`.') + w('None of them truncates anything, so none can be judged by `shown=`/`total=` and none should carry') + w('a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire.') + w('The distinction was named in review on #108 and the rows below now carry it.\n') w('The `class` column below carries that answer where it is known. **%d of %d caps are classified' - % (ix + op, len(caps))) - w('(%d INDEXING, %d OUTPUT); the remaining %d render `—`, which means NOT YET CLASSIFIED — never' - % (ix, op, len(caps) - ix - op)) - w('"neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry:') + % (ix + op + bd, len(caps))) + w('(%d INDEXING, %d OUTPUT, %d BOUNDARY); the remaining %d render `—`, which means NOT YET' + % (ix, op, bd, len(caps) - ix - op - bd)) + w('CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with') + w('a known expiry:') w('the tag belongs on the declaration itself, and this file exists only because the round that') w('produced the taxonomy could not touch `src/`. `test/limitstablecheck.sh` fails if a row there') w('names a cap that no longer exists.\n') diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index 7f8f7ca51..42831f6e0 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -15,29 +15,109 @@ # COVERAGE IS PARTIAL AND THE DOCUMENT SAYS SO. Only caps with a sourced classification are listed. # Everything else renders as an em dash, which reads as "not yet classified" — never as "neither". # Adding a row is a claim about what the cap bounds; make it by reading the call site, not the name. +# +# BOUNDARY — the third answer, added 2026-09-10. A BOUNDARY constant decides which SIDE of a rule +# something falls on; it truncates nothing, so it can never honestly emit a `capped="1"` and must not be +# judged by shown/total. `kUnitSizeLowRiskMax = 15` says "15 lines or fewer is low-risk"; +# `kMaxNameLen = 96` says a 97-character backticked token is a sentence, not an identifier; +# `kMaxPartitions = 16` bounds a hand-written `--partition=N`. Review on #108 named the class; labelling +# 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 kCoBoostMaxFilesPerCommit INDEXING kCoBoostMaxPartnerFiles INDEXING kCoBoostMaxSymbolsPerFile INDEXING kDocMentionMaxAnchors INDEXING kDocMentionMaxDocsPerAnchor INDEXING kDocMentionMaxDocsTotal INDEXING +kEditCheckSpellingsShown OUTPUT +kEvidenceCap OUTPUT kExpandMaxPer OUTPUT kExpandMaxSeeds OUTPUT +kFieldWalkCap INDEXING kHandoffCochangeRows OUTPUT kHandoffDocRows OUTPUT kHandoffNoteRows OUTPUT -kMaxPartitions OUTPUT +kHandoffSymbolsPerFile OUTPUT +kIdiomMaxCondTokens BOUNDARY +kIdiomMaxLabelTokens BOUNDARY +kIdiomMaxReturnTokens BOUNDARY +kMatchMaxHits INDEXING +kMaxAggsModeled INDEXING +kMaxAliasDepth INDEXING +kMaxAnchorsShown OUTPUT +kMaxAssertChars OUTPUT +kMaxBindings INDEXING +kMaxCacheBlobAgeDays BOUNDARY +kMaxCacheBlobCount BOUNDARY +kMaxCandidates INDEXING +kMaxChainDepth INDEXING +kMaxClaimedLine BOUNDARY +kMaxDecDigits BOUNDARY +kMaxDefsShown BOUNDARY +kMaxEditDistance BOUNDARY +kMaxEditLockAgeDays BOUNDARY +kMaxEnvNameLen BOUNDARY +kMaxExtLen BOUNDARY +kMaxFamily INDEXING +kMaxFieldsShown OUTPUT +kMaxFlipRows OUTPUT +kMaxFnsShown OUTPUT +kMaxHexDigits BOUNDARY +kMaxHunkSide INDEXING +kMaxIdentLen BOUNDARY +kMaxIdentsPerLine INDEXING +kMaxImportContainerDepth INDEXING +kMaxLineLen BOUNDARY +kMaxMacroDepth INDEXING +kMaxNameLen BOUNDARY +kMaxNamesTracked INDEXING +kMaxNearMisses OUTPUT +kMaxNestDepth INDEXING +kMaxPairsShown OUTPUT +kMaxPartitions BOUNDARY +kMaxPlanScout OUTPUT +kMaxProbeCommits INDEXING +kMaxQualifierHops INDEXING +kMaxRefs INDEXING +kMaxSample INDEXING +kMaxScopeChars OUTPUT +kMaxScored INDEXING +kMaxShown OUTPUT +kMaxSig OUTPUT +kMaxSitesShown OUTPUT +kMaxSkipRowsPerClass OUTPUT +kMaxStructsPerRef OUTPUT +kMaxStructsShown OUTPUT kMeasuredDigitsPricedWidth OUTPUT +kMemberSpellingsShown OUTPUT kMentionMaxDirectSymbols INDEXING kMentionMaxFiles INDEXING kMentionMaxRawTokens INDEXING kMentionMaxSymbolsPerFile INDEXING +kMinIdentLen BOUNDARY +kMinMentionLen BOUNDARY +kMinNameLen BOUNDARY +kMinValueNameLen BOUNDARY kNameCandidateCap OUTPUT +kOtherDefCap OUTPUT +kRadixThreshold BOUNDARY +kRenameMaxChain INDEXING +kRenameMaxPairs INDEXING +kSelectorFilesShown OUTPUT +kShown OUTPUT kSibliftMaxSeed OUTPUT kSibliftMaxSib OUTPUT +kSideDepthStd INDEXING +kSideDepthUses INDEXING +kSituBlastFilesShown OUTPUT +kSkillScanFindingCap INDEXING kSliceRdMaxIter OUTPUT kTestHopBasenameRowCap OUTPUT kTestHopCalleeRowCap OUTPUT -kUnitComplexityLowRiskMax OUTPUT -kUnitInterfacingLowRiskMax OUTPUT -kUnitSizeLowRiskMax OUTPUT +kType3MaxBucket INDEXING +kType3MaxTokensForLcs INDEXING +kUnitComplexityLowRiskMax BOUNDARY +kUnitInterfacingLowRiskMax BOUNDARY +kUnitSizeLowRiskMax BOUNDARY +kWhereisHits OUTPUT diff --git a/test/limitstablecheck.sh b/test/limitstablecheck.sh index 410b59040..87d0b6b96 100755 --- a/test/limitstablecheck.sh +++ b/test/limitstablecheck.sh @@ -1,10 +1,8 @@ #!/usr/bin/env bash # limitstablecheck.sh — docs/LIMITS.md is a BUILD PRODUCT of src/, and this gate says so. # -# WHY. A cap is a routing decision: it decides what an agent can and cannot find. This tree has 114 of -# them across 50 files — plus 6 ranking parameters partitioned out of the same census on 2026-09-10, -# which is why "120 constants" and "114 caps" are both right — and before 2026-09-09 nothing listed them -# together, so kMaxExpandSibs could sit +# WHY. A cap is a routing decision: it decides what an agent can and cannot find. Before 2026-09-09 +# nothing listed them together, so kMaxExpandSibs could sit # at 8, fire on 68.5% of bodies and hide 89.3% of every sibling name, justified by a cost ("~3.5 KB per # --pack-task bundle") that was not reproducible, because --pack-task emits no sibs= at all. Nobody was # wrong on purpose; the caps were simply never visible next to each other. @@ -27,12 +25,17 @@ # (F) THE SIDECAR IS LIVE. Every name in docs/limits_classes.tsv must be a cap that still exists in # src/. A sidecar keyed by name rots exactly this way, and a stale row is a classification applied # silently to nothing. Control: a fabricated row in a COPY of the sidecar must be refused. +# (H) THE DECLARATION SHAPE. `inline constexpr … = N;` on ONE line was never the shape of the +# population, only of one habit: `inline` is optional at namespace scope and forbidden on a class +# member. A plain `constexpr`, a `static constexpr` member and a wrapped initializer are each +# planted alone in a synthetic --root tree and required to appear, with a non-cap beside them +# required NOT to. Control: the same arm proves the NAME filter now admits `*PerFile`. # (G) THE COLUMN MATCHES THE SIDECAR. Every INDEXING/OUTPUT cell is read back out of the RENDERED # document and compared against the sidecar, so a classification cannot be right in the file and # wrong on the page. Control: flipping a class in a copy must move the rendered cell. # -# WHY (E)-(G) LIVE HERE. The 2026-09-10 round split this table in two — 114 caps that truncate, 6 -# parameters that weight — because they need different instruments: a cap is judged by what it cuts and +# WHY (E)-(G) LIVE HERE. The 2026-09-10 round split this table in two — caps that truncate, parameters +# that weight — because they need different instruments: a cap is judged by what it cuts and # gated by shown/total, a parameter by the eval that chose it. The split is only worth having if the # document cannot claim a source it does not have, and cannot claim a class the sidecar never gave it. set -u @@ -142,7 +145,7 @@ for line in open( os.path.join( ROOT, "docs", "limits_classes.tsv" ), encoding=" # The class cell is read back out of the RENDERED markdown, not out of the generator's own data # structures. Reading the artifact is the whole point: a column that is correct in memory and wrong on # the page is exactly the drift this arm exists for. -row = re.compile( r'^\| `(k[A-Za-z0-9_]*)` \| `[^`]*` \| \d+ \| (INDEXING|OUTPUT|—) \|' ) +row = re.compile( r'^\| `(k[A-Za-z0-9_]*)` \| `[^`]*` \| \d+ \| (INDEXING|OUTPUT|BOUNDARY|—) \|' ) got = {} for line in open( os.path.join( ROOT, "docs", "LIMITS.md" ), encoding="utf-8" ): m = row.match( line ) @@ -173,5 +176,48 @@ if re.search( r'^\| `%s` \| `[^`]*` \| \d+ \| %s \|' % ( re.escape( name ), cls ok( "(G) mutation control: flipping a sidecar row moves the rendered class, so (G) is not inert" ) CLASSCOL +# ── (H) THE DECLARATION SHAPE: a plain `constexpr` and a wrapped initializer are caps too ─────────── +# The register's first line says "Every compile-time cap in src/". It used to require the literal +# `inline constexpr` with the value on the SAME line, and 92 declarations — 81 distinct names — were +# outside it, among them kType3MaxBucket (bounds clone DETECTION), kSkillScanFindingCap (bounds a +# SECURITY verdict) and kChaConeCap. `inline` is optional at namespace scope and FORBIDDEN on a class +# member, so "inline constexpr" was never the shape of the population; it was the shape of one habit. +# +# Three fixtures, three ways a real cap is spelled, each planted alone in a synthetic --root tree and +# each required to appear in the generated table. A NON-cap name in the same file must NOT appear, or +# the arm would pass on a generator that admits everything. +for shape in plain static wrapped; do + d="$TMP/decl-$shape" + mkdir -p "$d/src" "$d/docs" + cp "$GEN" "$d/docs/limits_build.py" + case "$shape" in + plain) printf 'constexpr std::size_t kProbeRowCap = 3;\n' > "$d/src/probe.h" ;; + static) printf 'struct S\n{\n static constexpr std::size_t kProbeRowCap = 3;\n};\n' > "$d/src/probe.h" ;; + wrapped) printf 'inline constexpr std::size_t kProbeRowCap =\n 3;\n' > "$d/src/probe.h" ;; + esac + printf 'inline constexpr double kProbePlainConstant = 3.5;\n' >> "$d/src/probe.h" + if ! python3 "$d/docs/limits_build.py" --root "$d" --out "$d/docs/LIMITS.md" >/dev/null 2>&1; then + no "(H) $shape: the generator refused a tree whose only cap is spelled that way" + elif ! grep -Fq '`kProbeRowCap`' "$d/docs/LIMITS.md"; then + no "(H) $shape: a cap declared as \`$shape constexpr\` is INVISIBLE to the register" + elif grep -Fq '`kProbePlainConstant`' "$d/docs/LIMITS.md"; then + no "(H) $shape: a NON-cap constant was admitted — the census is too greedy to mean anything" + else + ok "(H) $shape: a cap spelled that way is found, and a non-cap beside it is not" + fi +done +# and the control that (H) is measuring the DECL regex and not the KEY one: a cap-shaped name the KEY +# vocabulary does not know must still be missed, or "the register found it" says nothing about how. +mkdir -p "$TMP/decl-key/src" "$TMP/decl-key/docs" +cp "$GEN" "$TMP/decl-key/docs/limits_build.py" +printf 'constexpr std::size_t kProbeRowCap = 3;\nconstexpr std::size_t kProbeSymbolsPerFile = 4;\n' \ + > "$TMP/decl-key/src/probe.h" +python3 "$TMP/decl-key/docs/limits_build.py" --root "$TMP/decl-key" --out "$TMP/decl-key/docs/LIMITS.md" >/dev/null 2>&1 +if grep -Fq '`kProbeSymbolsPerFile`' "$TMP/decl-key/docs/LIMITS.md"; then + ok "(H) control: the NAME filter admits PerFile too — kHandoffSymbolsPerFile is no longer invisible" +else + no "(H) control: a *PerFile cap is still outside the NAME filter, which is how kHandoffSymbolsPerFile" +fi + [ $fail -eq 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 9aa47afe9ad55193e790ae33852124f62ec3010b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:22:13 -0400 Subject: [PATCH 08/73] =?UTF-8?q?test(skills):=20the=20four=20restored=20s?= =?UTF-8?q?top=20rules=20are=20now=20measured=20=E2=80=94=20presence=20exa?= =?UTF-8?q?ctly,=20load-bearing=20differentially?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #112 restored four frontmatter STOP RULES and no row in test/skillevalfix/prompts.tsv could see any of them: stripping all four left split=test bm25-desc hit@1 byte-identical at 63.8% and split=dev 1.4pp BETTER, with skillevalcheck 15/15 green either way (2026-09-10 audit F-R1-03). That is the same failure #112 itself repaired — the fix restored the TEXT without adding a MEASUREMENT. The new stop-rule arm asserts two different things, because a stop rule can fail two ways: PRESENCE, exact — each sentence pinned in the gate verbatim; the arm's strip must actually remove it from that skill's SKILL.md, so a rewrite that drops OR REWORDS a rule makes the strip a no-op and the gate names which rule and stops. Words matched exactly, whitespace as \s+ (frontmatter folds; where the wrap falls is formatting, not the thing measured). LOAD-BEARING, differential — the 16 stop-rule rows scored against skills/ and against a stripped copy the gate builds itself; the real tree must win by >= 12.5pp. bm25-desc hit@1 with rules stripped the 16 stop-rule rows 75.0% 50.0% - the 8 that echo the rules (provenance=desc) 100.0% 50.0% - the 8 written to AVOID them (judged) 50.0% 50.0% whole corpus split=dev (n=99) 76.2% 72.6% whole corpus split=test (n=183, FROZEN) 63.8% 63.8% THE NULL IS REPORTED, NOT BURIED. The audit proposed rows "phrased without quoting it". Eight were written and measured: zero discrimination. A BM25 arm scores description TEXT, so it can only detect a sentence's removal through rows that share that sentence's words — "phrase it without quoting the rule" is not available to this instrument, and the exact-PRESENCE assertion is what covers what a lexical corpus cannot. Those 8 are kept as ordinary hard judged rows (4/8 route correctly; misses go to find-bug, write-tests, handoff, navigate). RED-FIRST: against a skills tree with the four sentences mechanically stripped, six arms fail (four PRESENCE, the absolute floor, the differential) while ALL 15 pre-existing arms still pass — precisely the F-R1-03 finding, now closed by construction. Corpus +16 rows, all split=dev by the header's own rule that the test split is FROZEN; skillevalsplitcheck confirms split=test hit@1 unchanged at 63.8%. Rows ASCII per the corpus rule. Seal 74953fd1a5e494f2805cb9c51cc828d59a4bf1f2c24912a069e1acdd2fa2a8ba (266 -> 282 rows). FLOORS NOT MOVED (a floor move is a deliberate recalibration commit). Slack as measured now: test hit@1 +11.8pp over floor 52.0, test sep-auc +0.071 over 0.83, dev hit@1 +17.2pp over floor 59.0, dev sep-auc +0.176 over 0.75. The dev pair is outside this gate file's own stated ~10pp / ~0.06-0.07 policy and is left as a NAMED owner decision (audit F-R1-10). Co-Authored-By: Claude Fable 5.1 --- docs/EVALS.md | 51 +++++++++++++++++++++++++ test/skillevalcheck.sh | 68 +++++++++++++++++++++++++++++++++ test/skillevalfix/PROVENANCE.md | 34 +++++++++++++++++ test/skillevalfix/prompts.tsv | 33 ++++++++++++++++ 4 files changed, 186 insertions(+) diff --git a/docs/EVALS.md b/docs/EVALS.md index 66569cd32..2ac0e0977 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -2205,6 +2205,57 @@ checks into their own `flowTaskChoice` function (mirroring the existing `instrum extraction) and by inlining the small filler-word loop directly rather than introducing a shared helper that collided token-for-token with `weakSymbolCandidate`'s existing shape. +### The four restored stop rules become measurable (2026-09-10) + +**The defect (audit F-R1-03).** #112 restored four frontmatter STOP RULES to the skill descriptions — +`before-you-build` "A small feature with an obvious home needs none of this.", `fresh-eyes` "A +single-lens question is a single call.", `orient` "Stop at the first rung that answers.", +`write-tests` "For one target one `--seams` or `--callers` pass suffices." — and **no row in +`test/skillevalfix/prompts.tsv` could see any of them**. Stripping all four left `split=test` bm25-desc +hit@1 byte-identical at 63.8% and `split=dev` **1.4pp better**, with `skillevalcheck` 15/15 green +either way. That is the same failure #112 itself repaired: the fix restored the TEXT without adding a +MEASUREMENT, so the next rewrite that drops a stop rule ships green. + +**What now measures them.** 16 rows between the `STOP-RULE ROWS (2026-09-10)` markers in the corpus +(all `split=dev` — the test split is frozen; `test/skillevalsplitcheck.sh` confirms `split=test` +hit@1 unchanged at 63.8%), plus a new **stop-rule arm** in `test/skillevalcheck.sh` that asserts two +different things, because a stop rule can fail two different ways: + +1. **PRESENCE, exact.** Each sentence is pinned in the gate verbatim, and the arm's strip must actually + remove it from that skill's `SKILL.md`. A rewrite that drops OR REWORDS a rule makes its strip a + no-op and the gate names which rule and stops. Whitespace between words is matched as `\s+` because + frontmatter folds — the wrap position is formatting, not the thing being measured. +2. **LOAD-BEARING, differential.** The 16 rows are scored against `skills/` and against a mechanically + stripped copy the gate builds itself; the real tree must win by ≥12.5pp. + +| measurement (bm25-desc hit@1) | with the four rules | stripped | +| --- | ---: | ---: | +| the 16 stop-rule rows | **75.0%** | 50.0% | +| — of which the 8 that echo the rules (`desc`) | **100.0%** | 50.0% | +| — of which the 8 written to avoid them (`judged`) | 50.0% | **50.0%** | +| whole corpus, `split=dev` (n=99) | **76.2%** | 72.6% | +| whole corpus, `split=test` (n=183, frozen) | 63.8% | 63.8% | + +**The null result is the interesting one, and it is reported rather than buried.** The audit's own +caveat was that its 8 prompts echo the stop rules' vocabulary and are therefore `desc`-shaped, and it +proposed rows "phrased without quoting it". Eight such rows were written and measured: **50.0% with the +rules and 50.0% without — zero discrimination.** A BM25 arm scores description TEXT, so it can only +detect a sentence's removal through rows that share that sentence's words. "Phrase it without quoting +the rule" is not available to this instrument; the exact-PRESENCE assertion above is what covers the +case a lexical corpus cannot. The 8 rows are kept as ordinary hard judged rows (4/8 route correctly — +their misses go to `find-bug`, `write-tests`, `handoff` and `navigate`, which is its own signal about +how the descriptions read a "one lens only" request phrased in a user's words). + +**Red-first.** Against a skills tree with the four sentences mechanically stripped, six arms fail +(four PRESENCE, the absolute floor, the differential) while **all 15 pre-existing arms still pass** — +which is precisely the F-R1-03 finding, now closed by construction. + +**Floors were NOT moved.** A floor move is a deliberate recalibration commit. Slack as measured after +this round: `split=test` hit@1 63.8% vs floor 52.0 (+11.8pp), sep-auc 0.901 vs 0.83 (+0.071); +`split=dev` hit@1 76.2% vs floor 59.0 (+17.2pp), sep-auc 0.926 vs 0.75 (+0.176). The dev pair is +outside the gate file's own stated policy (~10pp, ~0.06–0.07) and is left as a named owner decision +(audit F-R1-10). + ### `--help-task` weak-tier precision: the self-confirming gate and the config-key read (2026-09-10) **The defect, in one sentence each.** `does` was a symbol-slot cue AND `how does` is the diff --git a/test/skillevalcheck.sh b/test/skillevalcheck.sh index 455562f73..823dc25ee 100755 --- a/test/skillevalcheck.sh +++ b/test/skillevalcheck.sh @@ -185,5 +185,73 @@ awk -v v="$aucd" 'BEGIN{exit !(v+0 >= 0.75)}' \ && ok "dev-split bm25-desc sep-auc = ${aucd} (floor 0.75)" \ || no "dev-split bm25-desc sep-auc = ${aucd} fell under 0.75" +# ── 13) the four frontmatter STOP RULES are PRESENT and LOAD-BEARING ───────────────────────────────── +# 2026-09-10 audit F-R1-03: #112 restored four stop rules to the skill descriptions, and NO row in this +# corpus could see them. Deleting all four left split=test bm25-desc hit@1 byte-identical (63.8%) and +# split=dev 1.4pp BETTER, with all 15 arms above green — the same failure #112 itself repaired, still +# open, because the fix restored the TEXT without adding a measurement. +# +# Two assertions, because a stop rule can fail in two different ways: +# (a) PRESENCE, exact. Each sentence is pinned here verbatim. The strip below must actually remove +# something from each of the four descriptions; a rewrite that drops or REWORDS a rule makes its +# strip a no-op, and this arm says which one and stops. This is the half that catches the defect +# directly, and it cannot be fooled by a corpus that happens to score the same either way. +# (b) LOAD-BEARING, differential. The same 16 stop-rule rows are scored twice — against skills/ and +# against a mechanically stripped copy — and the real tree must win by a margin. This is what +# proves (a) is guarding something that matters rather than a decorative sentence. +# Measured on this commit: 75.0% with the rules, 50.0% without (n=16). Floors: 65.0% absolute (10pp +# under measured, the file's own header rule) and a >= 12.5pp gap (half the measured 25.0pp). +# HONEST LIMIT, stated because the number would otherwise read as more than it is: 8 of the 16 rows echo +# the rules' own wording and carry all of the discrimination; the 8 written to AVOID that wording score +# 50.0% with the rules and 50.0% without — measured, not assumed. A lexical ranker can only detect a +# sentence's removal through rows that share its words, so "phrase it without quoting the rule" is not +# available to this instrument. See the marker block in prompts.tsv. +STOPTSV="$TMP/stoprules.tsv" +awk -F'\t' 'BEGIN{p=0} /STOP-RULE ROWS \(2026-09-10/{p=1;next} /STOP-RULE ROWS . END/{p=0} p && !/^#/ && NF>=3' "$CORPUS" >"$STOPTSV" +stopRows=$( wc -l <"$STOPTSV" | tr -d ' ' ) +stopSkills=$( awk -F'\t' '{print $2}' "$STOPTSV" | sort -u | wc -l | tr -d ' ' ) +{ [ "$stopRows" = 16 ] && [ "$stopSkills" = 4 ]; } \ + && ok "stop-rule rows sliced from the corpus: ${stopRows} rows over ${stopSkills} skills" \ + || no "stop-rule slice found ${stopRows} rows / ${stopSkills} skills (want 16 / 4) — the marker block moved or shrank" +NOSTOP="$TMP/skills_nostop" +rm -rf "$NOSTOP"; cp -R "$SKILLS" "$NOSTOP" +strip_rule(){ # $1 = skill dir, $2 = the sentence, verbatim + local f="$NOSTOP/$1/SKILL.md" + [ -f "$f" ] || { no "stop-rule arm: no SKILL.md for $1"; return; } + python3 - "$f" "$2" <<'PY' +import re, sys +# The frontmatter FOLDS: a description is a wrapped YAML block, so a stop rule can straddle a newline + +# indent ("Stop at the first rung\n that answers."). Match the sentence word-for-word with any run of +# whitespace between words — exact on the WORDS, tolerant of where the wrap happens to fall, which is a +# formatting fact and not the thing this arm measures. +path, sentence = sys.argv[1], sys.argv[2] +text = open(path, encoding="utf-8").read() +pattern = re.compile(r"\s+".join(re.escape(w) for w in sentence.split())) +found = pattern.search(text) +if not found: + sys.exit(3) +open(path, "w", encoding="utf-8").write(text[:found.start()] + text[found.end():]) +PY + case $? in + 0) ok "stop rule PRESENT in $1: \"${2:0:44}...\"";; + 3) no "stop rule MISSING from $1 — the sentence this arm measures is no longer in the description: \"$2\"";; + *) no "stop-rule strip failed for $1";; + esac +} +strip_rule ripwire-before-you-build 'A small feature with an obvious home needs none of this.' +strip_rule ripwire-fresh-eyes 'A single-lens question is a single call.' +strip_rule ripwire-orient 'Stop at the first rung that answers.' +strip_rule ripwire-write-tests 'For one target one --seams or --callers pass suffices.' +"$BIN" "$SKILLS" --eval-skills="$STOPTSV" --no-cache >"$TMP/stop.on" 2>/dev/null +"$BIN" "$NOSTOP" --eval-skills="$STOPTSV" --no-cache >"$TMP/stop.off" 2>/dev/null +stopOn=$( awk '$1=="bm25-desc"{gsub("%","",$2); print $2}' "$TMP/stop.on" ) +stopOff=$( awk '$1=="bm25-desc"{gsub("%","",$2); print $2}' "$TMP/stop.off" ) +awk -v v="$stopOn" 'BEGIN{exit !(v+0 >= 65.0)}' \ + && ok "stop-rule rows route with the rules present: bm25-desc hit@1 = ${stopOn}% (floor 65.0%)" \ + || no "stop-rule rows fell to ${stopOn}% (floor 65.0%) — a stop rule stopped doing its job" +awk -v on="$stopOn" -v off="$stopOff" 'BEGIN{exit !((on+0)-(off+0) >= 12.5)}' \ + && ok "the rules are LOAD-BEARING: ${stopOn}% with them vs ${stopOff}% without (gap floor 12.5pp)" \ + || no "stripping all four stop rules moved hit@1 only ${stopOn}% -> ${stopOff}% — this arm measures nothing" + [ $fail -eq 0 ] && echo "skillevalcheck: ALL PASS" || echo "skillevalcheck: FAILURES" exit $fail diff --git a/test/skillevalfix/PROVENANCE.md b/test/skillevalfix/PROVENANCE.md index 50711ad2a..b2ea76238 100644 --- a/test/skillevalfix/PROVENANCE.md +++ b/test/skillevalfix/PROVENANCE.md @@ -23,3 +23,37 @@ the sealed set states the digest it measured against. Rows 266, split=test 183 (85 judged), split=dev 83 — unchanged; only the 12 labels moved. The description-budget round's held-out measurement was taken against the previous seal (16b1c847…); the folded arm (C2) was scored against a derived copy carrying exactly this map. + +## Seal after the 2026-09-10 stop-rule round (lane/helptask-precision; +16 rows, all split=dev) + + Seal: sha256(prompts.tsv) = 74953fd1a5e494f2805cb9c51cc828d59a4bf1f2c24912a069e1acdd2fa2a8ba + +Rows 266 → 282; split=test 183 (85 judged) **unchanged — the freeze holds**, split=dev 83 → 99. +The 16 new rows sit between the `STOP-RULE ROWS (2026-09-10)` markers and exist to measure the four +frontmatter STOP RULES #112 restored, which no row in this corpus could previously see (2026-09-10 +audit F-R1-03: deleting all four left split=test bm25-desc hit@1 byte-identical and split=dev 1.4pp +BETTER, with `skillevalcheck` 15/15 green either way). + +- **8 rows, `provenance=desc`** — the audit's own stop-rule prompts, ASCII-transliterated (em dash → + hyphen) per this file's ASCII rule and otherwise verbatim. They are `desc`, not `judged`: they echo + the rules' wording, which is exactly what `desc` means here, and the honesty matters because that + echo is where all of the discrimination lives. +- **8 rows, `provenance=judged`** — written for this round to AVOID the rules' wording. **Measured at + exactly zero discrimination: bm25-desc 50.0% with the four rules and 50.0% without** (n=8, same + binary, same rows). A BM25 arm scores description TEXT, so it can only detect a sentence's removal + through rows that share that sentence's words; "phrase it without quoting the rule" is not available + to this instrument. The rows are kept as ordinary hard judged rows and as the record of that null. +- `split=dev` for all 16, by the corpus header's own rule: the test split is FROZEN and new rows + belong in dev. `test/skillevalsplitcheck.sh` confirms split=test bm25-desc hit@1 is unchanged at + 63.8%. + +**Measured (bm25-desc hit@1, same binary):** the 16 rows alone 75.0% with the rules vs 50.0% without; +the whole corpus split=dev 76.2% vs 72.6% (3.6pp — the corpus can now see them at all); split=test +63.8% vs 63.8% (unchanged, as the freeze requires). `test/skillevalcheck.sh`'s new stop-rule arm +scores both trees itself and is RED on six arms against a stripped copy. + +**Floors were NOT moved this round** (a floor move is a deliberate recalibration commit, not a +side-effect). Slack as measured now: test hit@1 63.8% vs floor 52.0 (**+11.8pp**), test sep-auc 0.901 +vs 0.83 (+0.071), dev hit@1 76.2% vs floor 59.0 (**+17.2pp**), dev sep-auc 0.926 vs 0.75 (**+0.176**). +The dev pair remains outside the file's own stated policy (~10pp / ~0.06–0.07) and is left as a named +decision for the owner. diff --git a/test/skillevalfix/prompts.tsv b/test/skillevalfix/prompts.tsv index a92e25821..cd23cb975 100644 --- a/test/skillevalfix/prompts.tsv +++ b/test/skillevalfix/prompts.tsv @@ -333,3 +333,36 @@ Fill in the sprint retro doc with what shipped. none neg test Convert the images in assets to WebP. none neg dev Explain Python's walrus operator. none neg test Pin the GitHub Action to a commit SHA instead of a tag. none neg dev +# ── STOP-RULE ROWS (2026-09-10, lane/helptask-precision) — BEGIN. Sliced by test/skillevalcheck.sh's +# stop-rule arm between these two markers; the arm asserts exactly 16 rows, 4 per rule, so a silent +# slice failure fails the gate. Each row's ground truth depends on one of the four frontmatter STOP +# RULES #112 restored — sentences that no other row in this corpus could see (2026-09-10 audit F-R1-03: +# stripping all four left split=test hit@1 byte-identical and split=dev 1.4pp BETTER). split=dev by the +# corpus header's own rule that the test split is FROZEN and new rows belong in dev. +# before-you-build "A small feature with an obvious home needs none of this." +# fresh-eyes "A single-lens question is a single call." +# orient "Stop at the first rung that answers." +# write-tests "For one target one --seams or --callers pass suffices." +# PROVENANCE, stated honestly: the first 8 (the audit's own) echo the rules' vocabulary, which is what +# `desc` means in this file — they are labelled `desc`, not `judged`, because a lexical ranker can only +# detect a sentence's removal through rows that share its words. The second 8 were written to AVOID that +# vocabulary and are `judged`; they were MEASURED at exactly zero discrimination (bm25-desc 50.0% with +# the rules and 50.0% without), which is the honest answer to "phrase it without quoting it": for a BM25 +# arm, you cannot. They are kept as ordinary hard judged rows and as the record of that null result. +This feature is small and I already know exactly which file it belongs in - do I still need a plan? ripwire-before-you-build desc dev +A one-file addition with an obvious home, nothing multi-symbol about it ripwire-before-you-build desc dev +Just one lens please: which files are the hotspots, nothing else ripwire-fresh-eyes desc dev +A single question about this inherited module - where is the rot, one answer is enough ripwire-fresh-eyes desc dev +Give me the first rung only - what are the main subsystems, then stop ripwire-orient desc dev +Landing cold here; one map is all I want before I start reading ripwire-orient desc dev +One target, one pass: which tests reach parseFrame and which do not ripwire-write-tests desc dev +This single function has no coverage - one call to find the seam is enough ripwire-write-tests desc dev +This addition is tiny and I already know which module owns it - is a full plan worth it? ripwire-before-you-build judged dev +Do I really need the whole planning pass for a change this contained? ripwire-before-you-build judged dev +I only want the hotspot list for this inherited service, nothing more. ripwire-fresh-eyes judged dev +Where is the dead code in this module? That is the only thing I am asking. ripwire-fresh-eyes judged dev +New to this repo - the top-level subsystems and entry points, and I will take it from there. ripwire-orient judged dev +I want just enough of a map to start reading, not a full tour. ripwire-orient judged dev +There is exactly one function I need coverage for - what is the shortest way to find its seam? ripwire-write-tests judged dev +One untested helper, one pass to find what reaches it, done. ripwire-write-tests judged dev +# ── STOP-RULE ROWS — END ──────────────────────────────────────────────────────────────────────────── From e246ca251503c8093f83d9991cf316debe666d9e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:22:42 -0400 Subject: [PATCH 09/73] =?UTF-8?q?perf(ingest):=20the=20preprocessor-dead?= =?UTF-8?q?=20walk=20was=20O(C^2)=20=E2=80=94=20llvm=20cold=20202.14=20->?= =?UTF-8?q?=20170.46=20s=20CPU,=20map=20byte-identical?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit collectPreprocDeadRanges (src/preprocdead.h) read its children with ts_node_child( n, i ), which restarts tree-sitter's child iterator from the first child on every call, so the walk was O(C^2) in a node's child count — audit P1-0, the highest finding of the 2026-09-10 round. The repo already states that rule and ships the cursor helper for it, but the helper lived inside ingest.cpp's unnamed namespace (ingest_metrics.h) where preprocdead.h — compiled into slice.h too — could not reach it. So the helper moves to src/infra/tschildren.h, gains an appendChildren form for the DFS-stack case, and the walk uses it. Same child set, same left-to-right collection order, same reverse-child visit order, same emitted ranges. Why it hid: preprocdead.h short-circuits on src.find("#if") == npos, so Go/Python/JS corpora never enter the walk and test/padscalecheck.sh's comment-flood fixture (which has no #if) cannot reach it. Every C/C++ INCLUDE GUARD opens that gate and makes the guard's preproc_ifdef node one node whose child list is the whole file. A/B, one interleaved cold pair, llvm-project (2.9 GB, 182k files), same box, load ~12: arm CPU (user+sys) wall max RSS map pre 202.14 s 26.60 s 6.16 GB 43485 B new 170.46 s 18.99 s 6.23 GB 43485 B byte-identical delta -15.7% -28.6% Leaf attribution, 12 s sample of a cold llvm run (audit pre-figures vs this lane's post-figures): ts_node_child_iterator_next 62.99% -> 13.44% of busy collectPreprocDeadRanges (incl.) 56.67% -> 2.07% of busy The 31.7 s realised is short of the 107 s the 56.67% share implies: that share was read from a 12 s window of a 26 s run, and a leaf share is not a whole-run share. The A/B is the number to believe. Generated fixture (include guard + N line comments + an #if 0/#else pair), pre binary: N pre CPU new CPU 1000 0.01 s 0.01 s 4000 0.09 s 0.01 s 16000 1.24 s 0.01 s 124x Isolating control — the identical 16000-line flood with the guard REMOVED (no #if text, so the walk never runs) costs 0.02 s on the pre binary, i.e. the whole 1.24 s was this one loop. Narrow-tree corpora are inside the box's noise band, in both directions: paired interleaved cold runs gave ripwire's own tree +2.8% median (20 pairs) and the go corpus -2.1% median (12 pairs) — and go contains no #if at all, so the walk provably never runs there. No claim is made either way. Non-degradation — every one byte-identical, pre binary vs new, --no-cache: corpus map(--top-k=100000) --for --grep --pack-task --dead-code --lint ripwire (this tree) ok ok ok ok ok ok go (227 MB) ok ok ok ok ok ok canyonraid48/canyon (C++) ok ok ok ok ok ok llvm-project (map) ok Plus determinism (two cold runs cmp-equal) and xmllint --noout on the map. Gate (written first, red before the code): test/preprocdeadscalecheck.sh. Arms (B) 62.0x CPU for 16x the child width and (C) an include guard costing 62.0x the identical unguarded flood both FAILED against the pre-change binary and PASS after; (A) asserts the dead-range set through --uses on the flooded fixture (both halves: the #if 0 call absent, the live call present) plus determinism; (D1)/(D2) hold byte-identity against RIPWIRE_REF_BIN on the generated fixtures and on five committed C/C++ fixture trees; (E) shows every verdict and row reader able to fail. Its header carries the FOLLOW-UP enumeration of the 52 surviving indexed ts_node_child( n, i ) sites in three trip classes — 10 unbounded (class 1, the same defect), 5 input-controlled but small (class 2), ~37 grammar-bounded and correct as written (class 3). Not converted here: they live in files other lanes are editing. Verified: preprocdeadscalecheck, padscalecheck, preproccondcheck, blindspotcheck, slicecheck, includeprecisecheck, rustimportprecisecheck, loopconservationcheck, readmedriftcheck, cacheidentitycheck, limitstablecheck, infraportcheck, includeanglecheck, selfcontainedcheck, shellgateindexcheck, binoverridecheck, nodekindcheck, xmlwellformed, manifestcheck, gatecountcheck — all ALL PASS. ASan (-fno-sanitize-recover=all, LSan with the committed suppressions) clean on the generated fixtures, test/preproccondfix and ripwire's own tree, and the gate passes under it — the cursor's ts_tree_cursor_delete lifetime is RAII-held by ChildCursor. --quality-delta gating=0 (5 minor new-symbol api-surface rows: the moved helper is now a named rw:: symbol instead of an internal-linkage one — my footprint, deliberately not acked). Gate count regenerated to 587 by docs/gatecount_build.py, never hand-written. --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/infra/tschildren.h | 69 +++++++++ src/ingest_metrics.h | 38 +---- src/preprocdead.h | 34 ++++- test/preprocdeadscalecheck.sh | 271 +++++++++++++++++++++++++++++++++ test/regression.sh | 2 +- 8 files changed, 384 insertions(+), 46 deletions(-) create mode 100644 src/infra/tschildren.h create mode 100755 test/preprocdeadscalecheck.sh diff --git a/README.md b/README.md index dde8da76a..d157ea9f3 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-586 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +587 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 **586 gate scripts** and is the authoritative list; +`test/regression.sh` names **587 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..1b1058118 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` | 586 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 587 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) @@ -5625,7 +5625,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 **586 gate scripts**, all of which exist on disk. +naming **587 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`. @@ -6637,7 +6637,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 586. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 587. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 0647c42fe..fc5a0220f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["586 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 + ["587 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "586", "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, "587", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["586 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/infra/tschildren.h b/src/infra/tschildren.h new file mode 100644 index 000000000..3da5d5cf0 --- /dev/null +++ b/src/infra/tschildren.h @@ -0,0 +1,69 @@ +#pragma once +// tschildren.h — O(children) child collection for UNBOUNDED-WIDTH tree-sitter walks. +// +// WHY THIS EXISTS. `ts_node_child( n, i )` restarts tree-sitter's child iterator from the FIRST child on +// every call (vendored `ts_node__child`, third_party/deps/tree_sitter/lib/src/node.c:139 — it builds a +// fresh `ts_node_iterate_children` each time and calls `ts_node__relevant_child_count` on each skipped +// invisible child), so an indexed loop over a node's C children costs O(C²). Width is +// attacker-controlled: ONE 980 KB file of 14 000 line comments hands the root 14 000 children and turned +// ingest into ~2 s of user CPU, quadratic in line count (gate: test/padscalecheck.sh). Every +// unbounded-width walk therefore collects the child list ONCE per node with a TSTreeCursor — the same +// child set (named + anonymous + extras) in the same left-to-right order, O(C) total. The cursor and the +// out vector are caller-owned and reused across nodes, so a warm walk allocates nothing per node. +// Bounded-shape scans (base clauses, argument lists, a declaration's declarators) keep the indexed form — +// their widths come from the grammar, not from the input file. +// +// WHY IT IS ITS OWN HEADER AND NOT A SECTION OF ingest.cpp. It was one, inside ingest_metrics.h's unnamed +// namespace, and that made it unreachable from the two headers that ALSO walk whole subtrees and are +// compiled outside that translation unit — src/preprocdead.h (shared with src/slice.h). The result was +// audit P1-0 (2026-09-10): `collectPreprocDeadRanges` kept the indexed form, and on a cold llvm-project +// run `ts_node_child_iterator_next` was 62.99% of busy leaves with that ONE walk's inclusive subtree at +// 56.67% of busy CPU. Converting it took that cold run from 202.14 s CPU to 170.46 s (wall 26.60 -> +// 18.99 s) with a byte-identical map. A rule that only some translation units can obey is a rule that +// gets broken, so the rule and the helper now live where every walk can reach them. Gates: +// test/padscalecheck.sh (the comment flood) and test/preprocdeadscalecheck.sh (the include-guard flood, +// which the `#if`-text gate in preprocdead.h hides from the first). + +#include + +#include + +namespace rw +{ + +struct ChildCursor // RAII — several walkers return mid-loop, so deletion must not depend on fallthrough +{ + TSTreeCursor cur; + explicit ChildCursor( TSNode n ) noexcept : cur( ts_tree_cursor_new( n ) ) {} + ChildCursor( const ChildCursor& ) = delete; + ChildCursor& operator=( const ChildCursor& ) = delete; + ~ChildCursor() { ts_tree_cursor_delete( &cur ); } +}; + +// APPEND n's children, left to right, to whatever `out` already holds. This is the form a DFS-STACK walk +// needs: there the collected list IS the work list, so clearing it would throw the frontier away. Routing +// such a walk through collectChildren instead costs it a scratch vector plus a copy of every node; the two +// forms measured indistinguishably on this box (both inside a ±3% noise band that a same-binary control +// reproduced with the opposite sign), so this exists for the shape, not for a measured win. +inline void appendChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates +{ + ts_tree_cursor_reset( &cur, n ); + if( ts_tree_cursor_goto_first_child( &cur ) ) + { + do + { + out.push_back( ts_tree_cursor_current_node( &cur ) ); + } + while( ts_tree_cursor_goto_next_sibling( &cur ) ); + } +} + +// REPLACE `out` with n's children — the form a walker uses when it wants one node's child list as a +// standalone array to scan or index. Delegates, so there is exactly one spelling of the cursor idiom. +inline void collectChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates +{ + out.clear(); + appendChildren( n, cur, out ); +} + +} // namespace rw diff --git a/src/ingest_metrics.h b/src/ingest_metrics.h index 5b2de028f..c2206090f 100644 --- a/src/ingest_metrics.h +++ b/src/ingest_metrics.h @@ -5,7 +5,8 @@ // ingest_metrics.h — the per-definition structural metrics, moved VERBATIM from ingest.cpp in the // 2026-08-29 split: cyclomatic complexity (Myers' &&/|| extension), cognitive complexity with its -// nesting/hump accounting, the O(children) child collection (ChildCursor/collectChildren), the +// nesting/hump accounting (the O(children) child collection it used to hold, ChildCursor/collectChildren, +// now lives in src/infra/tschildren.h so walks outside this TU can obey the same rule), the // essential-complexity ev(G) single-exit reduction (CtrlNode arena, EvCtx, the why-tag taxonomy), // the local-variable-indexing walk (ln_*), the fused complexityOf DFS, and parameter/arity counting // (countParams, cc_paramArityExact, callArity). Pure metric machinery: reads an AST, fills RawDef @@ -179,35 +180,12 @@ inline bool cc_isBooleanJoin( TSNode n, std::string_view src, Lang lang ) noexce } // ── O(children) child collection for whole-subtree walks ───────────────────────────────────────────── -// ts_node_child( n, i ) restarts tree-sitter's child iterator from the FIRST child on every call, so an -// indexed loop over a node's C children costs O(C²). Width is attacker-controlled: ONE 980 KB file of -// 14 000 line comments hands the root 14 000 children and turned ingest into ~2 s of user CPU, quadratic -// in line count (gate: test/padscalecheck.sh). Every unbounded-width walk below therefore collects the -// child list ONCE per node with a TSTreeCursor — the same child set (named + anonymous + extras) in the -// same left-to-right order, O(C) total. The cursor and the out vector are caller-owned and reused across -// nodes, so a warm walk allocates nothing per node. Bounded-shape scans (base clauses, argument lists) -// keep the indexed form — their widths come from the grammar, not from the input file. -struct ChildCursor // RAII — several walkers return mid-loop, so deletion must not depend on fallthrough -{ - TSTreeCursor cur; - explicit ChildCursor( TSNode n ) noexcept : cur( ts_tree_cursor_new( n ) ) {} - ChildCursor( const ChildCursor& ) = delete; - ChildCursor& operator=( const ChildCursor& ) = delete; - ~ChildCursor() { ts_tree_cursor_delete( &cur ); } -}; -inline void collectChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates -{ - out.clear(); - ts_tree_cursor_reset( &cur, n ); - if( ts_tree_cursor_goto_first_child( &cur ) ) - { - do - { - out.push_back( ts_tree_cursor_current_node( &cur ) ); - } - while( ts_tree_cursor_goto_next_sibling( &cur ) ); - } -} +// ChildCursor / collectChildren MOVED to src/infra/tschildren.h (audit P1-0, 2026-09-10). They were +// defined here, inside this TU's unnamed namespace, which put them out of reach of the whole-subtree +// walks compiled outside ingest.cpp — src/preprocdead.h kept the indexed O(C²) form for exactly that +// reason and cost 56.67% of a cold llvm run. Unqualified lookup from this unnamed namespace still +// finds rw::collectChildren, so every call site below is unchanged; the rule they enforce, and why a +// bounded-shape scan keeps the indexed form, are stated in full on the new header. // bounded-depth search for a structured_binding_declarator anywhere under `n` — the vendored tree-sitter-cpp // grammar nests it TWO levels below the `declaration` node (declaration -> init_declarator -> diff --git a/src/preprocdead.h b/src/preprocdead.h index df5f96239..c2ba51d4c 100644 --- a/src/preprocdead.h +++ b/src/preprocdead.h @@ -36,6 +36,8 @@ #include +#include "infra/tschildren.h" // P1-0: ChildCursor/collectChildren — the O(C) child collection this walk needs + namespace rw { @@ -95,8 +97,29 @@ inline PreprocLiteral preprocLiteralBranch( TSNode n, std::string_view src ) noe // covers it. The dead ALTERNATIVE of `#if 1` runs from the start of that chain to the end of the node. // // Ranges may overlap and are NOT merged: the only consumer asks "does this byte fall in any of them", and -// merging would be work done for no reader. Deterministic — pre-order, no map iteration, no allocation -// beyond the output vector. +// merging would be work done for no reader. Deterministic — one fixed DFS order, no map iteration. +// +// THE WALK IS CURSOR-BASED, NOT INDEXED (audit P1-0, 2026-09-10). It used to read its children with +// `ts_node_child( n, i )`, which restarts tree-sitter's child iterator from the first child on every call +// and so costs O(C²) in a node's child count — the exact rule stated on src/infra/tschildren.h, broken +// here because the helper that enforces it used to be reachable only from inside ingest.cpp. The `#if` +// text gate below is what hid it: no `#if` anywhere in a file means no walk at all, so Go/Python/JS +// corpora never pay it — but every C/C++ header on earth opens that gate with its INCLUDE GUARD, which +// then makes the guard's own preproc_ifdef node one node whose child list is the whole file. MEASURED, +// cold llvm-project, one interleaved pair on the same box: 202.14 s CPU / 26.60 s wall -> 170.46 s / +// 18.99 s, map byte-identical; this function's inclusive share of busy leaf samples 56.67% -> 2.07%, and +// `ts_node_child_iterator_next` 62.99% -> 13.44% (the residue is the OTHER indexed walks, itemised in +// test/preprocdeadscalecheck.sh's FOLLOW-UP block). The 31.7 s realised is well short of the 107 s the +// 56.67% share implies, because that share was read from a 12 s window of a 26 s run and a leaf share is +// not a whole-run share — the A/B is the number to believe. Same child set, same left-to-right order, +// same emitted ranges — only the cost changed. Gate: +// test/preprocdeadscalecheck.sh, whose arm (C) is the isolating control (the identical comment flood with +// and without an include guard) and whose (D1)/(D2) arms hold the byte-identity against the indexed form. +// +// Children are pushed in index order and popped from the back, so a node's children are VISITED in +// reverse; that is the order the indexed form had and it is preserved deliberately. Nothing downstream +// reads the range vector in order (inPreprocDead is a membership test), but "the ranges are the same +// SET" is a weaker claim than "the output is byte-identical", and only the second one is gateable. inline std::vector collectPreprocDeadRanges( TSNode root, std::string_view src ) { std::vector out; @@ -108,6 +131,7 @@ inline std::vector collectPreprocDeadRanges( TSNode root, std: } std::vector stack; + ChildCursor cursor( root ); // reused across nodes — ts_tree_cursor_reset re-points it at each one stack.push_back( root ); while( !stack.empty() ) { @@ -133,11 +157,7 @@ inline std::vector collectPreprocDeadRanges( TSNode root, std: } } - const std::uint32_t kids = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < kids; ++i ) - { - stack.push_back( ts_node_child( n, i ) ); - } + appendChildren( n, cursor.cur, stack ); // O(C), not O(C²) — index order in, reverse order out } return out; } diff --git a/test/preprocdeadscalecheck.sh b/test/preprocdeadscalecheck.sh new file mode 100755 index 000000000..400e178f4 --- /dev/null +++ b/test/preprocdeadscalecheck.sh @@ -0,0 +1,271 @@ +#!/usr/bin/env bash +# preprocdeadscalecheck.sh — the PREPROCESSOR-DEAD walk's SCALING gate, and the ranges it must still yield. +# +# bash test/preprocdeadscalecheck.sh # build/ripwire +# bash test/preprocdeadscalecheck.sh build/ripwire_base # the RED run (base binary walks with ts_node_child) +# RIPWIRE_BIN=asan/ripwire bash test/preprocdeadscalecheck.sh +# RIPWIRE_REF_BIN=/path/to/pre-change/ripwire bash test/preprocdeadscalecheck.sh # arms (D1)/(D2) +# +# WHY A SECOND SCALING GATE. test/padscalecheck.sh already asserts that the comment-flood pathology +# (ONE file whose root holds tens of thousands of children) stays linear across the ingest walks it +# reaches. It cannot reach THIS one: `collectPreprocDeadRanges` (src/preprocdead.h) short-circuits on +# `src.find( "#if" ) == npos`, and padscalecheck's fixture contains no `#if` at all — so its 28 000-line +# file never enters the walk. Every C/C++ header in the world carries an INCLUDE GUARD, which is what +# opens that gate in practice, so the pathology hid behind a text test that real corpora always defeat. +# Measured on llvm-project (audit P1-0, 2026-09-10): `ts_node_child_iterator_next` = 62.99% of busy +# leaves of a cold run and this one walk's inclusive subtree = 56.67% of busy CPU, ~107 s of 188 s. +# +# WHAT THE THREE FIXTURES ARE FOR. `guard/nN` = an include guard + N line comments + one `#if 0`/`#else` +# pair + one real function. The guard makes the whole file ONE preproc_ifdef node whose child list is +# N wide and INPUT-controlled, which is exactly the width `ts_node_child( n, i )` costs O(C^2) to index. +# `plain/n16000` is the same flood with the guard removed: byte-for-byte the same parse work, the same +# node widths, and NO `#if` text — so the difference between the two is the preproc-dead walk and +# nothing else. That control is why arm (C) can name this walk rather than "ingest got slower". +# +# ARMS +# (A) RANGES — the dead-range SET itself, through the only surface that exposes it: a call inside +# `#if 0` is not served as a live role="call" row by --uses while the live call to the same callee +# in the same file still is. BOTH halves, because "no dead row" also passes on an empty answer. +# Asserted on the FLOODED fixture, so it is the converted wide walk that produced the ranges. +# (B) SCALING — user CPU, guard/n1000 vs guard/n16000. 16x the width must not cost ~256x the CPU. +# (C) ISOLATION — guard/n16000 vs plain/n16000 (same flood, no `#if` text). This is the arm that +# names the walk; it does not short-circuit on a fast absolute number the way (B) does. +# (D1) BYTE-IDENTICAL vs RIPWIRE_REF_BIN on the generated fixtures — the conversion must not move +# one byte of output. (D2) the same against the committed C/C++ fixture trees. Both SKIPPED and +# disclosed when RIPWIRE_REF_BIN is unset (a gate cannot hold a "before" binary of its own). +# (E) MUTATION — every verdict shape above is shown able to fail, against hand-built inputs. +# +# User CPU, never wall, so a loaded box cannot flake the ratios; and every ratio floors its divisor so a +# ~0 s small arm cannot manufacture a large one. +# +# Exit 0 = ALL PASS, non-zero = SOME FAILED. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative RIPWIRE_BIN +REF="${RIPWIRE_REF_BIN:-}" +[ -n "$REF" ] && [ "${REF#/}" = "$REF" ] && REF="$ROOT/$REF" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +skip(){ printf ' SKIP %s\n' "$*"; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "preprocdeadscalecheck: python3 required"; exit 2; } +echo "preprocdeadscalecheck: BIN=$BIN" +[ -n "$REF" ] && echo "preprocdeadscalecheck: REF=$REF" + +# FOLLOW-UP (audit P1-0, 2026-09-10) — this gate covers src/preprocdead.h ONLY. 52 further indexed +# `ts_node_child( n, i )` sites survive in src/ (`grep -rn 'ts_node_child( ' src/ | grep -v _count`). +# Every one was read and classified; the TRIP CLASS is what sets the loop's C, and only class 1 is a +# defect. They live in files other lanes are editing this round, so the list is the deliverable, not the +# diff; the full table with the reasoning is in the lane report. +# +# CLASS 1 — per node, width from the FILE (the O(C^2) shape this lane just fixed; convert next): +# src/ingest_astquery.h:1226 collectSpanTiers whole-subtree stack walk, ALL children, from the root +# src/pattern.h:1007 findMatches whole-subtree stack walk, ALL children, from the root +# src/pattern.h:429 smallestContaining descends level by level, all children at each level +# src/pattern.h:493 snapshotNode collects all children of an arbitrary matched node +# src/pattern.h:875 matchChildren collects all children of an arbitrary candidate node +# src/slice.h:1158 sliceWalk whole-subtree recursion over all children +# src/slice.h:1090 sliceWalkPreproc all children of a preproc node — the SAME include-guard +# width this lane just measured at 62x +# src/ingest_crawl.h:516 measureFileHealth whole-subtree stack walk (ERROR/MISSING hunt) +# src/ingest_metrics.h:1347 ln_collectLocalDecls recursion over all children (depth-capped 512, +# width uncapped) +# src/ingest_sidecap.h:199 ffiVisitNode every decl of an `extern "C" { … }` block +# CLASS 2 — per node, width from the INPUT but small in practice (measure before converting): +# src/ingest_astquery.h:1807 collectGatedLocalNames (one def's re-parsed top level) +# src/ingest_relations.h:1768 capturePythonImportBinds (names in one import statement) +# src/ingest_sidecap.h:477 routesVisitNode (decorators on one definition) +# src/ingest_binds.h:1343 bindsVisitNode (declarators of one `declaration`) +# src/ingest_names.h:61 firstChildOfType (early-returning search, any node) +# CLASS 3 — width from the GRAMMAR, correct as written, do NOT convert (the remaining ~37): +# base clauses, parameter/argument lists, type annotations, attribute lists, fixed-index probes +# (`ts_node_child( n, 0 )`), and every `cc_*`/`ev_*` fixed-shape scan. See the note on +# src/infra/tschildren.h for why a grammar-bounded width keeps the indexed form. + +# ── fixtures ───────────────────────────────────────────────────────────────────────────────────────── +# Generated, never committed: a committed 1 MB comment flood would join every OTHER gate's view of test/ +# (trap: a gate fixture that is also part of the live tree the tool indexes). +gen(){ # $1 = dir, $2 = comment line count, $3 = "guard" | "plain" + mkdir -p "$1" + python3 - "$1/big.c" "$2" "$3" <<'PY' +import sys +path, n, mode = sys.argv[ 1 ], int( sys.argv[ 2 ] ), sys.argv[ 3 ] +L = [] +if mode == "guard": + L += [ "#ifndef RIPWIRE_SCALE_GUARD_H", "#define RIPWIRE_SCALE_GUARD_H", "" ] +L += [ "int target( int x );" ] +L += [ "// pad " + "x" * 60 ] * n +L += [ "int liveCaller( int x )", "{", " return target( x );", "}" ] +if mode == "guard": + L += [ "int deadCaller( int x )", "{", " return 0;", "#if 0", + " return target( x );", "#endif", "}" ] +L += [ "int worker( void ) { return 424242; }" ] +if mode == "guard": + L += [ "#endif" ] +open( path, 'w' ).write( "\n".join( L ) + "\n" ) +PY +} +gen "$TMP/guard/n1000" 1000 guard +gen "$TMP/guard/n4000" 4000 guard +gen "$TMP/guard/n16000" 16000 guard +gen "$TMP/plain/n16000" 16000 plain + +# user-CPU seconds (user+sys) of one cold ingest of $1 +usercpu(){ # $1 = corpus dir, $2 = binary + { /usr/bin/time -p "$2" "$1" --no-cache >/dev/null; } 2>"$TMP/t" || { echo FAIL; return; } + awk '/^user/ { u = $2 } /^sys/ { s = $2 } END { printf "%.2f", u + s }' "$TMP/t" +} + +# The one ratio verdict, so no arm hand-rolls a second arithmetic for the same job. Prints +# "fast" | "linear" | "quad ". $1 small, $2 big, $3 ratio ceiling, $4 absolute short-circuit. +verdict(){ awk -v s="$1" -v b="$2" -v cap="$3" -v floor="$4" 'BEGIN { + if( b + 0 < floor + 0 ) { print "fast"; exit } # absolute cost already fine — scaling is moot + if( s + 0 < 0.02 ) { s = 0.02 } # floor the divisor: a ~0 arm cannot invent a ratio + if( b / s < cap + 0 ) { printf "linear %.1f", b / s } else { printf "quad %.1f", b / s } +}'; } + +# ── (A) the ranges themselves — asserted on the FLOODED fixture ─────────────────────────────────────── +echo +echo "=== (A) the dead-range SET: a call inside \`#if 0\` is not a live call, the \`#else\`-side one is ===" +"$BIN" "$TMP/guard/n1000" --no-cache --uses=big.c:target >"$TMP/a_uses.xml" 2>/dev/null +A_ROOT="$( python3 -c ' +import re,sys +d=open(sys.argv[1]).read() +m=re.search(r"]*>",d) +sys.stdout.write(m.group(0) if m else "")' "$TMP/a_uses.xml" )" +rows(){ python3 -c ' +import re,sys +d=open(sys.argv[1]).read() +sys.stdout.write("\n".join(r for r in re.findall(r"]*>",d) if sys.argv[2] in r))' "$1" "$2"; } +if [ -z "$A_ROOT" ]; then + no "(A) no root element (empty capture — the arm reading it would have been vacuous)" +else + A_LIVE="$( rows "$TMP/a_uses.xml" liveCaller )" + A_DEAD="$( rows "$TMP/a_uses.xml" deadCaller )" + if [ -z "$A_LIVE" ]; then + no "(A) control broken — the LIVE call from liveCaller is missing; the dead-row half would be vacuous" + elif [ -n "$A_DEAD" ]; then + no "(A) a call site inside \`#if 0\` is served as a live row: $A_DEAD (#62)" + else + ok "(A) live call present, \`#if 0\` call absent (on a 1000-child preproc_ifdef node)" + fi +fi +# determinism of the same answer — the walk order is part of the output contract +"$BIN" "$TMP/guard/n1000" --no-cache --uses=big.c:target >"$TMP/a_uses2.xml" 2>/dev/null +if [ ! -s "$TMP/a_uses.xml" ]; then + no "(A) determinism (empty --uses answer)" +elif cmp -s "$TMP/a_uses.xml" "$TMP/a_uses2.xml"; then + ok "(A) determinism (two cold --uses runs byte-identical, $( wc -c <"$TMP/a_uses.xml" | tr -d ' ' ) B)" +else + no "(A) determinism (two cold --uses runs differ)" +fi + +# ── (B) scaling: 16x the child width must not cost ~256x the CPU ───────────────────────────────────── +echo +echo "=== (B) scaling across child width (1000 -> 16000 children of one preproc_ifdef) ===" +b_small="$( usercpu "$TMP/guard/n1000" "$BIN" )" +b_big="$( usercpu "$TMP/guard/n16000" "$BIN" )" +if [ "$b_small" = FAIL ] || [ "$b_big" = FAIL ] || [ -z "$b_small" ] || [ -z "$b_big" ]; then + no "(B) scaling (a timed ingest run failed outright)" +else + v="$( verdict "$b_small" "$b_big" 24 1.0 )" + case "$v" in + fast) ok "(B) 16000-child ingest ${b_big}s CPU < 1.0s — the O(C^2) walk is absent";; + linear\ *) ok "(B) ${v#linear } x CPU for 16x the width (small=${b_small}s big=${b_big}s; linear ~16, quadratic ~256)";; + *) no "(B) ${v#quad } x CPU for 16x the width — the O(children^2) walk is back (small=${b_small}s big=${b_big}s)";; + esac +fi + +# ── (C) isolation: the SAME flood without `#if` text must cost about the same ──────────────────────── +echo +echo "=== (C) isolation: guarded vs unguarded flood of identical width (the preproc-dead walk alone) ===" +c_plain="$( usercpu "$TMP/plain/n16000" "$BIN" )" +if [ "$c_plain" = FAIL ] || [ -z "$c_plain" ] || [ "$b_big" = FAIL ] || [ -z "$b_big" ]; then + no "(C) isolation (a timed ingest run failed outright)" +else + v="$( verdict "$c_plain" "$b_big" 8 0.30 )" + case "$v" in + fast) ok "(C) guarded flood ${b_big}s CPU < 0.30s — the walk costs nothing measurable (unguarded ${c_plain}s)";; + linear\ *) ok "(C) ${v#linear } x the unguarded flood (guarded=${b_big}s plain=${c_plain}s) — under the 8x ceiling";; + *) no "(C) an include guard costs ${v#quad } x the identical unguarded flood — collectPreprocDeadRanges is quadratic (guarded=${b_big}s plain=${c_plain}s)";; + esac +fi + +# ── (D) byte-identical against a reference binary ──────────────────────────────────────────────────── +echo +echo "=== (D) byte-identical output vs RIPWIRE_REF_BIN ===" +if [ -z "$REF" ]; then + skip "(D1)/(D2) RIPWIRE_REF_BIN unset — no reference binary to compare against (set it to the pre-change build)" +elif [ ! -x "$REF" ]; then + no "(D) RIPWIRE_REF_BIN=$REF is not executable" +else + d_fail=0 + for d in "$TMP/guard/n1000" "$TMP/guard/n4000" "$TMP/guard/n16000" "$TMP/plain/n16000"; do + "$BIN" "$d" --no-cache --top-k=100000 >"$TMP/d_new" 2>/dev/null + "$REF" "$d" --no-cache --top-k=100000 >"$TMP/d_ref" 2>/dev/null + if [ ! -s "$TMP/d_ref" ]; then + no "(D1) reference map of $( basename "$d" ) is empty — the comparison would be vacuous"; d_fail=1 + elif ! cmp -s "$TMP/d_new" "$TMP/d_ref"; then + no "(D1) map of $( basename "$d" ) differs from the reference binary"; d_fail=1 + fi + done + [ "$d_fail" = 0 ] && ok "(D1) all four generated fixtures map byte-identically to the reference" + d_fail=0 + d_seen=0 + for d in "$ROOT/test/preproccondfix" "$ROOT/test/cfix" "$ROOT/test/cppqualfix" "$ROOT/test/cudafix" "$ROOT/test/metalfix"; do + [ -d "$d" ] || continue + d_seen=$(( d_seen + 1 )) + "$BIN" "$d" --no-cache --top-k=100000 >"$TMP/d_new" 2>/dev/null + "$REF" "$d" --no-cache --top-k=100000 >"$TMP/d_ref" 2>/dev/null + if [ ! -s "$TMP/d_ref" ]; then + no "(D2) reference map of $( basename "$d" ) is empty — the comparison would be vacuous"; d_fail=1 + elif ! cmp -s "$TMP/d_new" "$TMP/d_ref"; then + no "(D2) map of $( basename "$d" ) differs from the reference binary"; d_fail=1 + fi + done + if [ "$d_seen" = 0 ]; then + no "(D2) no committed C/C++ fixture tree found — the arm would have been vacuous" + elif [ "$d_fail" = 0 ]; then + ok "(D2) $d_seen committed C/C++ fixture trees map byte-identically to the reference" + fi +fi + +# ── (E) mutation: every verdict shape above is shown able to fail ──────────────────────────────────── +echo +echo "=== (E) MUTATION — the verdict and row readers are shown able to fail ===" +case "$( verdict 0.01 2.56 24 1.0 )" in + quad\ *) ok "(E) B-shape: a quadratic pair (0.01s -> 2.56s) IS called quad";; + *) no "(E) the (B) verdict cannot see a quadratic pair";; +esac +case "$( verdict 0.10 1.60 24 1.0 )" in + linear\ *) ok "(E) B-shape: a 16x pair (0.10s -> 1.60s) IS called linear";; + *) no "(E) the (B) verdict calls a linear pair quadratic";; +esac +case "$( verdict 0.02 1.16 8 0.30 )" in + quad\ *) ok "(E) C-shape: the measured pre-change isolation pair (0.02s vs 1.16s) IS called quad";; + *) no "(E) the (C) verdict cannot see the pathology it was written against";; +esac +case "$( verdict 0.01 0.20 8 0.30 )" in + fast) ok "(E) C-shape: a sub-0.30s big arm short-circuits to fast";; + *) no "(E) the (C) absolute short-circuit does not fire";; +esac +printf '' >"$TMP/m_d.xml" +if [ -n "$( rows "$TMP/m_d.xml" deadCaller )" ] && [ -n "$( rows "$TMP/m_d.xml" liveCaller )" ]; then + ok "(E) A-shape: a served \`#if 0\` row IS detected when one is present" +else + no "(E) the (A) row reader cannot see rows that are present" +fi +if [ -n "$( rows "$TMP/m_d.xml" noSuchCaller )" ]; then + no "(E) the (A) row reader invents rows that are absent" +else + ok "(E) A-shape: an absent caller name yields no rows" +fi + +echo +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/regression.sh b/test/regression.sh index 0d54307d8..b97013cff 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preprocdeadscalecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 236a1ad5491d6068f9ef3bfb39155a7d50b45fd9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:24:47 -0400 Subject: [PATCH 10/73] perf(ingest): the field NAME resolved once per grammar, not once per AST node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ts_node_child_by_field_name( n, "name", 4 )` — 199 sites across the ingest walk sections, --slice and the preprocessor reader — does not look a field up. It looks the field's NAME up first, by a linear `strncmp` scan over the grammar's whole field table (third_party/.../language.c:226), and then calls `ts_node_child_by_field_id` with the answer (node.c:773). The answer is a pure function of ( grammar, field name ) and never changes, so this is loop-invariant work recomputed per AST node — and it is OPTREMARKS F3's defect one layer down: `strncmp` is an EXTERNAL libc symbol, so LTO cannot reach it, and on macOS each comparison goes through DYLD-STUB$$strncmp then DYLD-STUB$$_platform_strncmp first. MEASURED BEFORE (1 ms `sample`, go corpus cold --no-cache, 18,963 busy leaf samples): `strncmp` + both dyld stubs 3.44% of busy; the `ts_node_child_by_field_name` subtree 4.70%, of which 93.6% is owned by ONE caller — cc_boolOp, which cc_walk asks twice per AST node. src/infra/fieldid.h resolves all 41 field spellings once per grammar into a [grammar][field] TSFieldId table at the ingest prewarm (warmFieldIdTable(), beside the compiled-query prewarm and under the same single-writer / lock-free-reader invariant), and `fieldChild( n, NodeField::Name )` reads it. An unwarmed grammar falls back to resolving by name: today's path, today's answer, today's cost — never a wrong node. A/B — CPU (user+sys via rusage), interleaved with the arms swapped at the half, one unrecorded warm-up per arm, `--no-cache --top-k=100000`: | corpus | n | A median | B median | delta median | delta min | B.med -586 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +587 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 **586 gate scripts** and is the authoritative list; +`test/regression.sh` names **587 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..1b1058118 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` | 586 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 587 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) @@ -5625,7 +5625,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 **586 gate scripts**, all of which exist on disk. +naming **587 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`. @@ -6637,7 +6637,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 586. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 587. 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 237a4eeb5..19e3c2a42 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -10,10 +10,10 @@ where the pathological tail is, never near the typical case — and when it fire | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 114 | 50 | 79 | **35** | +| 115 | 51 | 79 | **36** | Plus 6 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 114 + 6 is the 120 constants this generator parses out of `src/`. +are not counted as caps, and 115 + 6 is the 121 constants this generator parses out of `src/`. ## INDEXING or OUTPUT — which half of the answer a cap bounds @@ -23,8 +23,8 @@ flag, no budget, no second call gets the answer back, and the output reads as "n `--detail`, a page or a follow-up call can recover the answer. The two are not the same severity and a single table that does not distinguish them invites fixing the cheap one first. -The `class` column below carries that answer where it is known. **26 of 114 caps are classified -(10 INDEXING, 16 OUTPUT); the remaining 88 render `—`, which means NOT YET CLASSIFIED — never +The `class` column below carries that answer where it is known. **26 of 115 caps are classified +(10 INDEXING, 16 OUTPUT); the remaining 89 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 produced the taxonomy could not touch `src/`. `test/limitstablecheck.sh` fails if a row there @@ -215,6 +215,14 @@ Discloses: **none** | --- | --- | --- | --- | --- | | `kBlankSpellingMaxCodePoints` | `8` | 215 | — | — | +### `src/infra/fieldid.h` + +Discloses: **none** + +| constant | value | line | class | note | +| --- | --- | --- | --- | --- | +| `kFieldIdCapacity` | `64` | 118 | — | — | + ### `src/infra/profilePmc.h` Discloses: **none** @@ -493,10 +501,10 @@ Discloses: **none** | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kSliceFlowDefaultDepth` | `8` | 2094 | — | the disclosed default bound (depth= always states it) | -| `kSliceFlowDepthMax` | `32` | 2098 | — | — | -| `kSliceFlowDepthMin` | `1` | 2097 | — | — | -| `kSliceRdMaxIter` | `64` | 1205 | OUTPUT | — | +| `kSliceFlowDefaultDepth` | `8` | 2095 | — | the disclosed default bound (depth= always states it) | +| `kSliceFlowDepthMax` | `32` | 2099 | — | — | +| `kSliceFlowDepthMin` | `1` | 2098 | — | — | +| `kSliceRdMaxIter` | `64` | 1206 | OUTPUT | — | ### `src/slicediff.h` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 0647c42fe..fc5a0220f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["586 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 + ["587 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "586", "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, "587", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["586 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/infra/fieldid.h b/src/infra/fieldid.h new file mode 100644 index 000000000..19e1a692f --- /dev/null +++ b/src/infra/fieldid.h @@ -0,0 +1,197 @@ +#pragma once +// fieldid.h — read a tree-sitter node's FIELD child without re-deriving the field's id from its name. +// +// WHY THIS EXISTS. `ts_node_child_by_field_name( n, "name", 4 )` — 199 call sites across the ingest +// walk sections, `--slice` and the preprocessor reader — does not look a field up. It looks the field's +// NAME up first, every time, and the lookup it runs is a linear `strncmp` scan over the grammar's whole +// field table (third_party/deps/tree_sitter/lib/src/language.c, `ts_language_field_id_for_name`): +// +// for( TSSymbol i = 1; i < count + 1; i++ ) { switch( strncmp( name, self->field_names[i], name_length ) ) { … } } +// +// The answer is a pure function of ( grammar, field name ) and never changes for the life of the +// process, so this is loop-invariant work recomputed per AST NODE. It is also F3's defect one layer +// down: `strncmp` is an EXTERNAL libc symbol, so LTO cannot reach it, and on macOS every one of those +// comparisons is a call through TWO dyld stubs — `DYLD-STUB$$strncmp` in our image, then +// `DYLD-STUB$$_platform_strncmp` in libsystem_platform — before `_platform_strncmp` starts. +// +// MEASURED, on the audit tree at 9356cf23 (docs/OPTREMARKS.md §8b/F3 is the same shape one layer up). +// A 1 ms `sample` of a cold `--no-cache` run over the go corpus (15,865 files), 18,963 busy leaf +// samples: `strncmp` + its two dyld stubs are **3.44 %** of busy CPU, the `ts_node_child_by_field_name` +// subtree is 4.70 %, and **93.6 % of that subtree is owned by one caller** — `cc_boolOp`, which +// `cc_walk` asks twice per AST node at the fall-through of its dispatch. The ripwire tree's own share +// is 2.56 % (it is markdown-heavy, so less of it is AST walk). +// +// WHAT IT REPLACES IT WITH. `fieldChild( n, NodeField::Name )` resolves the id ONCE PER GRAMMAR, at +// the ingest prewarm (`warmFieldIdTable()` in ingest_crawl.h, which owns the grammar table), into a +// [grammar][field] table of `TSFieldId`, and then calls `ts_node_child_by_field_id` directly. Three +// properties matter: +// +// • It is the SAME CALL the by-name form makes. `ts_node_child_by_field_name` is literally +// `ts_node_child_by_field_id( self, ts_language_field_id_for_name( self.tree->language, name, len ) )` +// (node.c:773). Removing the middle term cannot change the node returned — it removes a pure +// function's recomputation, not a decision. +// • A grammar that HAS NO SUCH FIELD keeps the by-name answer exactly. `ts_language_field_id_for_name` +// returns 0 for an unknown name, and `ts_node_child_by_field_id( n, 0 )` returns the null node on +// its first line (`if (!field_id || …) return ts_node__null();`, node.c:602). So a 0 in this table +// is not a hole to guard against — it IS the "this grammar has no `receiver:`" answer, and the +// single-language arms that rely on it (ingest_relations.h's Go/Ruby split, for one) keep working +// unchanged. test/fieldidcheck.sh arm C proves the parity rather than asserting it. +// • An UNWARMED grammar is correct, not broken. The lookup falls back to resolving by name — today's +// code path, today's answer, today's cost. Nothing here can return a wrong node because a warm was +// missed; it can only fail to be faster, which is what arm E of the gate is for. +// +// THREADING. The registry is written ONLY by `warmFieldIds`, and only from the single-threaded prewarm +// before any parse worker exists — the same invariant, and the same reason, as `compiledQueryCache()` +// in ingest_crawl.h: workers then only READ it, so no lock is on the per-node path. `warmFieldIds` is +// idempotent (a second warm of the same grammar is a no-op), so a caller does not have to know whether +// it ran. Do NOT call it from a worker thread. +// +// SCOPE. Every `ts_node_child_by_field_name` site in src/ is on this, per-node path or not — unlike +// F3's node-kind conversion, there is no cost to converting a cold site here (the call is not longer, +// and the enum is what the gate's population arm can see). test/fieldidcheck.sh arm E holds that at +// zero remaining sites. + +#include +#include +#include + +#include + +namespace rw +{ + +// The field names the tree actually asks for, harvested from the call sites this header replaced. +// ORDER IS THE CONTRACT: kNodeFieldNames below is indexed by this enum, and the static_assert under it +// is what keeps a name added in one place and not the other from compiling. Alphabetical so a new field +// has one obvious home — the enum carries no meaning beyond being an index. +enum class NodeField : std::uint8_t +{ + Alias, Alternative, Argument, Arguments, Attribute, + Body, Captures, Condition, Consequence, Constructor, + Declaration, Declarator, DefaultValue, Definition, Directive, + Field, Function, Initializer, Key, Left, + Method, ModuleName, Name, Object, Operator, + Parameter, Parameters, Path, Pattern, Property, + Receiver, Right, Scope, Source, Subject, + Superclasses, Target, Trait, Type, Update, + Value, + Count +}; + +inline constexpr std::size_t kNodeFieldCount = static_cast( NodeField::Count ); + +// The grammar spelling of each NodeField, and its length. The length is carried rather than recomputed +// because `ts_language_field_id_for_name` takes one and `std::strlen` on the fallback path would put the +// libc call back that this header exists to remove. +struct NodeFieldName +{ + const char* text; + std::uint32_t len; +}; + +inline constexpr std::array kNodeFieldNames = { { + { "alias", 5 }, { "alternative", 11 }, { "argument", 8 }, { "arguments", 9 }, { "attribute", 9 }, + { "body", 4 }, { "captures", 8 }, { "condition", 9 }, { "consequence", 11 }, { "constructor", 11 }, + { "declaration", 11 }, { "declarator", 10 }, { "default_value", 13 }, { "definition", 10 }, { "directive", 9 }, + { "field", 5 }, { "function", 8 }, { "initializer", 11 }, { "key", 3 }, { "left", 4 }, + { "method", 6 }, { "module_name", 11 }, { "name", 4 }, { "object", 6 }, { "operator", 8 }, + { "parameter", 9 }, { "parameters", 10 }, { "path", 4 }, { "pattern", 7 }, { "property", 8 }, + { "receiver", 8 }, { "right", 5 }, { "scope", 5 }, { "source", 6 }, { "subject", 7 }, + { "superclasses", 12 }, { "target", 6 }, { "trait", 5 }, { "type", 4 }, { "update", 6 }, + { "value", 5 }, +} }; + +static_assert( kNodeFieldNames.size() == kNodeFieldCount, "kNodeFieldNames must carry exactly one spelling per NodeField" ); + +// ---- the [grammar][field] table ------------------------------------------------------------------ +// +// SoA, not a map: the scan reads ONLY the grammar pointers, so they live in their own contiguous array +// and the id blocks never share a cache line with them. +// +// THE CAPACITY IS NOT A SILENT CAP. It is 64 against 23 distinct grammars over 47 extension rows today, +// and ingest_crawl.h carries a static_assert that the crawl table's ROW count — an upper bound on its +// distinct-grammar count — fits here, so a 65th language is a COMPILE error rather than a run that is +// quietly slower. test/fieldidcheck.sh arm F is the second guard, on the distinct count. The runtime +// overflow arm in warmFieldIds below is therefore unreachable from the product; it is kept because a +// caller outside the crawl table would otherwise be silently wrong instead of silently slow, and slow is +// the honest failure here — an unregistered grammar keeps the by-name path, which is today's answer. +inline constexpr std::size_t kFieldIdCapacity = 64; + +struct FieldIdRegistry +{ + std::array lang{}; // scanned — hot + std::array, kFieldIdCapacity> ids{}; // payload — read once, on the hit + std::size_t count{ 0 }; +}; + +// `constinit` is load-bearing, not decoration: a function-local static with dynamic initialization gets +// a guard variable, and the guard's acquire load would sit on the per-AST-node path this header exists +// to shorten. Zero-initialized aggregates need no guard, and constinit is what makes the compiler say so +// instead of leaving it to inspection. +inline FieldIdRegistry& fieldIdRegistry() noexcept +{ + static constinit FieldIdRegistry registry{}; + return registry; +} + +// Resolve every NodeField for one grammar and install it. Idempotent. SINGLE-THREADED ONLY — see the +// THREADING note at the top of this file. A `lang` of nullptr (the grammar-less markdown rows in the +// crawl table) is a no-op, so a caller can hand the whole table over without filtering it first. +inline void warmFieldIds( const TSLanguage* lang ) +{ + if( lang == nullptr ) + { + return; + } + FieldIdRegistry& registry = fieldIdRegistry(); + for( std::size_t i = 0; i < registry.count; ++i ) + { + if( registry.lang[ i ] == lang ) + { + return; // already warm — a second prewarm changes nothing + } + } + if( registry.count >= kFieldIdCapacity ) + { + return; // full: this grammar keeps the by-name path (correct, just not faster) + } + const std::size_t slot = registry.count; + for( std::size_t f = 0; f < kNodeFieldCount; ++f ) + { + registry.ids[ slot ][ f ] = ts_language_field_id_for_name( lang, kNodeFieldNames[ f ].text, kNodeFieldNames[ f ].len ); + } + registry.lang[ slot ] = lang; // published LAST: the pointer is what a reader scans for + registry.count = slot + 1; +} + +// How many grammars are warm. For gates and for --doctor; never on a hot path. +inline std::size_t warmedGrammarCount() noexcept +{ + return fieldIdRegistry().count; +} + +// The field id for ( grammar, field ) — the table's answer when the grammar is warm, and the same +// resolution the by-name call would have run when it is not. Never a wrong answer, only a slower one. +inline TSFieldId fieldIdFor( const TSLanguage* lang, NodeField field ) noexcept +{ + const std::size_t f = static_cast( field ); + const FieldIdRegistry& registry = fieldIdRegistry(); + for( std::size_t i = 0; i < registry.count; ++i ) + { + if( registry.lang[ i ] == lang ) + { + return registry.ids[ i ][ f ]; + } + } + return ts_language_field_id_for_name( lang, kNodeFieldNames[ f ].text, kNodeFieldNames[ f ].len ); +} + +// `ts_node_child_by_field_name( n, kNodeFieldNames[field].text, …len )`, with the name resolution hoisted +// out of the per-node path. Same deref of `n`'s tree, same null-node answer for a field the grammar does +// not have, same node otherwise. +inline TSNode fieldChild( TSNode n, NodeField field ) noexcept +{ + return ts_node_child_by_field_id( n, fieldIdFor( ts_node_language( n ), field ) ); +} + +} // namespace rw diff --git a/src/ingest.cpp b/src/ingest.cpp index 552ced851..00f7a2218 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -13,6 +13,7 @@ #include "quality.h" // A5: cacheDirLadder + sweepStaleCacheBlobsOnce — the cache-dir hygiene hook (saveCache) #include "embedded_queries.h" // configure-generated constexpr tags.scm table; no runtime source-tree dependency #include "infra/nodekind.h" // rw::kindIs - the inline node-kind compare the per-AST-node dispatch chains run on (OPTREMARKS F3) +#include "infra/fieldid.h" // rw::fieldChild - the same defect one layer down: the field NAME resolved once per grammar, not per node #include "infra/hashutil.h" // sanitizer-clean modulo-2^64 FNV multiplication #include "infra/namesplit.h" // H4: stripTemplateArgs for the C++ qualified-call re-split (shared with tracelocus.h) #include "infra/jsonesc.h" // rw::shSingleQuote - the git ignore probe quotes its root the same way every other git popen does @@ -265,6 +266,10 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud // read+hash (safe). v15: result.files is passed in because the blob's offset table lets the load // deserialise ONLY the records for the files THIS crawl asked for — a wider configuration's blob is // never walked past its table (docs/EVALS.md, the offset-table retry). + // The [grammar][field] TSFieldId table (src/infra/fieldid.h), filled before ANY thread exists. Every + // AST walk downstream — the parse pool, --slice, --lint, the preprocessor reader — reads it lock-free. + warmFieldIdTable(); + CacheLoadStats cacheStats; HashMap cache = cacheFile.empty() ? HashMap{} diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 0cd2e4297..8b58e1544 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -38,16 +38,16 @@ inline bool isMemberAccessNode( const char* t, Lang lang ) noexcept inline TSNode memberAccessReceiver( TSNode access, Lang lang ) noexcept { - if( lang == Lang::Python ) { return ts_node_child_by_field_name( access, "object", 6 ); } - if( lang == Lang::Ruby ) { return ts_node_child_by_field_name( access, "receiver", 8 ); } - return ts_node_child_by_field_name( access, "argument", 8 ); + if( lang == Lang::Python ) { return fieldChild( access, NodeField::Object ); } + if( lang == Lang::Ruby ) { return fieldChild( access, NodeField::Receiver ); } + return fieldChild( access, NodeField::Argument ); } inline TSNode memberAccessField( TSNode access, Lang lang ) noexcept { - if( lang == Lang::Python ) { return ts_node_child_by_field_name( access, "attribute", 9 ); } - if( lang == Lang::Ruby ) { return ts_node_child_by_field_name( access, "method", 6 ); } - return ts_node_child_by_field_name( access, "field", 5 ); + if( lang == Lang::Python ) { return fieldChild( access, NodeField::Attribute ); } + if( lang == Lang::Ruby ) { return fieldChild( access, NodeField::Method ); } + return fieldChild( access, NodeField::Field ); } // The classified receiver of one call site. `var` is set for NamedVar / FieldOfVar, `field` for @@ -90,7 +90,7 @@ inline RecvShape classifyReceiver( TSNode node, Lang lang, std::string_view src, } if( lang == Lang::Python && kindIs( rt, "call" ) ) { // Phase 5: `super().m()` / `super(C, self).m()` — the receiver is a CALL of the identifier `super` - const TSNode fn = ts_node_child_by_field_name( node, "function", 8 ); + const TSNode fn = fieldChild( node, NodeField::Function ); if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "identifier" ) && pattern::nodeText( fn, src ) == "super" ) { return { RecvKind::SuperObj, {}, {} }; @@ -207,7 +207,7 @@ inline std::string_view declaratorVarName( TSNode decl, std::string_view src ) return ( a <= b && b <= src.size() ) ? src.substr( a, b - a ) : std::string_view{}; } // unwrap a pointer/reference/parenthesized declarator to its inner `declarator` child - const TSNode inner = ts_node_child_by_field_name( decl, "declarator", 10 ); + const TSNode inner = fieldChild( decl, NodeField::Declarator ); if( ts_node_is_null( inner ) ) { return {}; @@ -225,7 +225,7 @@ inline std::string_view declaratorVarName( TSNode decl, std::string_view src ) inline std::string_view paramDeclaratorVarName( TSNode decl, std::string_view src ) { if( !ts_node_is_null( decl ) && kindIs( ts_node_type( decl ), "reference_declarator" ) - && ts_node_is_null( ts_node_child_by_field_name( decl, "declarator", 10 ) ) && ts_node_named_child_count( decl ) > 0 ) + && ts_node_is_null( fieldChild( decl, NodeField::Declarator ) ) && ts_node_named_child_count( decl ) > 0 ) { decl = ts_node_named_child( decl, 0 ); } @@ -246,11 +246,11 @@ inline std::string ctorTypeOf( TSNode value, std::string_view src ) TSNode idn {}; if( kindIs( vt, "call_expression" ) ) { // C++/TS `Foo()` - idn = ts_node_child_by_field_name( value, "function", 8 ); + idn = fieldChild( value, NodeField::Function ); } else if( kindIs( vt, "new_expression" ) ) { // C++/TS `new Foo()` - idn = ts_node_child_by_field_name( value, "constructor", 11 ); + idn = fieldChild( value, NodeField::Constructor ); } if( ts_node_is_null( idn ) ) { @@ -313,7 +313,7 @@ inline std::string fnBindTargetOf( TSNode value, std::string_view src, bool& was { return {}; } - idn = ts_node_child_by_field_name( value, "argument", 8 ); + idn = fieldChild( value, NodeField::Argument ); if( ts_node_is_null( idn ) ) { return {}; @@ -373,7 +373,7 @@ inline FnBindDeclShape fnDeclaratorShape( TSNode decl, std::string_view src ) shape.sawFn = shape.sawFn || kindIs( dt, "function_declarator" ); shape.sawPtr = shape.sawPtr || kindIs( dt, "pointer_declarator" ); shape.sawRef = shape.sawRef || isRef; - TSNode inner = ts_node_child_by_field_name( decl, "declarator", 10 ); + TSNode inner = fieldChild( decl, NodeField::Declarator ); if( ts_node_is_null( inner ) && ( isRef || kindIs( dt, "parenthesized_declarator" ) ) ) { // the parenthesized/reference inner declarator is an UNNAMED child — take the first named one @@ -405,17 +405,17 @@ inline std::string_view misparsedFnPtrDeclVar( TSNode lhs, std::string_view src { return {}; } - const TSNode inner = ts_node_child_by_field_name( lhs, "function", 8 ); + const TSNode inner = fieldChild( lhs, NodeField::Function ); if( ts_node_is_null( inner ) || !kindIs( ts_node_type( inner ), "call_expression" ) ) { return {}; } - const TSNode ty = ts_node_child_by_field_name( inner, "function", 8 ); + const TSNode ty = fieldChild( inner, NodeField::Function ); if( ts_node_is_null( ty ) || !kindIs( ts_node_type( ty ), "primitive_type" ) ) { return {}; } - const TSNode args = ts_node_child_by_field_name( inner, "arguments", 9 ); + const TSNode args = fieldChild( inner, NodeField::Arguments ); if( ts_node_is_null( args ) || ts_node_named_child_count( args ) != 1 ) { return {}; @@ -430,7 +430,7 @@ inline std::string_view misparsedFnPtrDeclVar( TSNode lhs, std::string_view src { return {}; } - const TSNode idn = ts_node_child_by_field_name( pe, "argument", 8 ); + const TSNode idn = fieldChild( pe, NodeField::Argument ); if( ts_node_is_null( idn ) || !kindIs( ts_node_type( idn ), "identifier" ) ) { return {}; @@ -559,17 +559,17 @@ inline std::string_view fnPtrAliasName( TSNode n, const char* t, std::string_vie { if( kindIs( t, "alias_declaration" ) ) { - const TSNode desc = ts_node_child_by_field_name( n, "type", 4 ); + const TSNode desc = fieldChild( n, NodeField::Type ); if( ts_node_is_null( desc ) ) { return {}; } - const TSNode abst = ts_node_child_by_field_name( desc, "declarator", 10 ); + const TSNode abst = fieldChild( desc, NodeField::Declarator ); if( ts_node_is_null( abst ) || !kindIs( ts_node_type( abst ), "abstract_function_declarator" ) ) { return {}; } - const TSNode nm = ts_node_child_by_field_name( n, "name", 4 ); + const TSNode nm = fieldChild( n, NodeField::Name ); return ts_node_is_null( nm ) ? std::string_view{} : nodeTextOf( nm, src ); } if( !kindIs( t, "type_definition" ) ) @@ -597,7 +597,7 @@ inline std::string_view fnPtrAliasName( TSNode n, const char* t, std::string_vie { crossed = true; } - TSNode inner = ts_node_child_by_field_name( d, "declarator", 10 ); + TSNode inner = fieldChild( d, NodeField::Declarator ); if( ts_node_is_null( inner ) && ts_node_named_child_count( d ) > 0 ) { inner = ts_node_named_child( d, 0 ); @@ -637,7 +637,7 @@ inline void collectFnBindTypeFacts( TSNode n, const char* t, std::string_view sr return; } std::string typeName; - const bool concrete = concreteWrittenType( ts_node_child_by_field_name( n, "type", 4 ), src, typeName ); + const bool concrete = concreteWrittenType( fieldChild( n, NodeField::Type ), src, typeName ); const auto [ scopeStart, scopeEnd ] = enclosingDefSpan( n ); const std::uint32_t cc = ts_node_child_count( n ); for( std::uint32_t i = 0; i < cc; ++i ) @@ -650,7 +650,7 @@ inline void collectFnBindTypeFacts( TSNode n, const char* t, std::string_view sr TSNode d = ts_node_child( n, i ); if( kindIs( ts_node_type( d ), "init_declarator" ) ) { - d = ts_node_child_by_field_name( d, "declarator", 10 ); + d = fieldChild( d, NodeField::Declarator ); } const FnBindDeclShape shape = fnDeclaratorShape( d, src ); if( shape.name.empty() || ( shape.sawFn && !shape.sawPtr ) ) @@ -852,7 +852,7 @@ inline void emitShadowVarDecls( std::uint32_t fileId, Lang lang, TSNode decl, st } return; } - TSNode inner = ts_node_child_by_field_name( decl, "declarator", 10 ); + TSNode inner = fieldChild( decl, NodeField::Declarator ); if( ts_node_is_null( inner ) && ( kindIs( dt, "reference_declarator" ) || kindIs( dt, "parenthesized_declarator" ) ) && ts_node_named_child_count( decl ) > 0 ) @@ -909,12 +909,12 @@ inline void emitShadowParamDecls( TSNode params, std::uint32_t fileId, Lang lang continue; // commas, `...`, attribute nodes — nothing declared } bodySite.startByte = ts_node_start_byte( p ); - const TSNode declarator = ts_node_child_by_field_name( p, "declarator", 10 ); + const TSNode declarator = fieldChild( p, NodeField::Declarator ); emitShadowVarDecls( fileId, lang, declarator, src, bodySite, binds ); // member-variable round (card A3): the parameter's WRITTEN type as a ParamType record (`Counter& c` → // c:Counter), read by the field use-site index alone — see LocalBindKind::ParamType. `auto`, templated // and decltype types write nothing (writtenTypeOf's own refusal), and pushRawBind drops the record. - pushRawBind( fileId, lang, paramDeclaratorVarName( declarator, src ), writtenTypeOf( ts_node_child_by_field_name( p, "type", 4 ), src ), + pushRawBind( fileId, lang, paramDeclaratorVarName( declarator, src ), writtenTypeOf( fieldChild( p, NodeField::Type ), src ), BindSite{ ts_node_start_byte( p ), 0u, 0u }, LocalBindKind::ParamType, binds ); } } @@ -928,16 +928,16 @@ inline void emitShadowParamDecls( TSNode params, std::uint32_t fileId, Lang lang inline void captureLambdaShadowDecls( TSNode n, std::uint32_t fileId, Lang lang, std::string_view src, BindSite bodySite, std::vector& binds ) { - const TSNode d = ts_node_child_by_field_name( n, "declarator", 10 ); // abstract_function_declarator + const TSNode d = fieldChild( n, NodeField::Declarator ); // abstract_function_declarator if( !ts_node_is_null( d ) ) { - const TSNode params = ts_node_child_by_field_name( d, "parameters", 10 ); + const TSNode params = fieldChild( d, NodeField::Parameters ); if( !ts_node_is_null( params ) ) { emitShadowParamDecls( params, fileId, lang, src, bodySite, binds ); } } - const TSNode caps = ts_node_child_by_field_name( n, "captures", 8 ); // lambda_capture_specifier + const TSNode caps = fieldChild( n, NodeField::Captures ); // lambda_capture_specifier const std::uint32_t cc = ts_node_is_null( caps ) ? 0u : ts_node_named_child_count( caps ); for( std::uint32_t i = 0; i < cc; ++i ) { @@ -970,16 +970,16 @@ inline void captureLambdaShadowDecls( TSNode n, std::uint32_t fileId, Lang lang, // parameters and a fn-pointer TYPE's parameter list out of shadow evidence. inline TSNode fnDefParameterList( TSNode fnDef ) { - TSNode decl = ts_node_child_by_field_name( fnDef, "declarator", 10 ); + TSNode decl = fieldChild( fnDef, NodeField::Declarator ); for( int guard = 0; guard < 8 && !ts_node_is_null( decl ) && !kindIs( ts_node_type( decl ), "function_declarator" ); ++guard ) { - decl = ts_node_child_by_field_name( decl, "declarator", 10 ); + decl = fieldChild( decl, NodeField::Declarator ); } if( ts_node_is_null( decl ) || !kindIs( ts_node_type( decl ), "function_declarator" ) ) { return TSNode{}; } - return ts_node_child_by_field_name( decl, "parameters", 10 ); + return fieldChild( decl, NodeField::Parameters ); } // r9 shadow suppression (A5 fix round): the local-declaring shapes that live OUTSIDE `declaration` nodes @@ -1004,7 +1004,7 @@ inline TSNode fnDefParameterList( TSNode fnDef ) // second rule to keep in step). Plain, typed, defaulted and splat parameters; tuple patterns bind nothing. inline void capturePythonParamShadowDecls( TSNode n, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) { - const TSNode params = ts_node_child_by_field_name( n, "parameters", 10 ); + const TSNode params = fieldChild( n, NodeField::Parameters ); if( ts_node_is_null( params ) ) { return; @@ -1021,7 +1021,7 @@ inline void capturePythonParamShadowDecls( TSNode n, std::uint32_t fileId, Lang } else if( kindIs( pt, "default_parameter" ) || kindIs( pt, "typed_default_parameter" ) ) { - ident = ts_node_child_by_field_name( p, "name", 4 ); + ident = fieldChild( p, NodeField::Name ); } else if( kindIs( pt, "typed_parameter" ) || kindIs( pt, "list_splat_pattern" ) || kindIs( pt, "dictionary_splat_pattern" ) ) { @@ -1057,7 +1057,7 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file { return; // every other node type declares nothing this capture owns } - const TSNode body = ts_node_child_by_field_name( n, "body", 4 ); + const TSNode body = fieldChild( n, NodeField::Body ); if( ts_node_is_null( body ) ) { return; // a body-less shape scopes nothing (declaration-only lambda/definition never parses so) @@ -1068,12 +1068,12 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file // iteration 3, unified with enclosingShadowScope's control-statement rule: the loop variable scopes // to the WHOLE for_range_loop statement (its own span), not merely the body. const BindSite loopSite{ ts_node_start_byte( n ), ts_node_start_byte( n ), ts_node_end_byte( n ) }; - const TSNode loopDeclarator = ts_node_child_by_field_name( n, "declarator", 10 ); + const TSNode loopDeclarator = fieldChild( n, NodeField::Declarator ); emitShadowVarDecls( fileId, lang, loopDeclarator, src, loopSite, binds ); // member-variable round (card A3): the loop variable's WRITTEN type (`for( const Symbol& s : v )` → // s:Symbol) as a ParamType record for the field use-site index — the single most common typed // receiver shape in this repo's own source (`s.name`), and `auto` writes nothing, as for parameters. - pushRawBind( fileId, lang, paramDeclaratorVarName( loopDeclarator, src ), writtenTypeOf( ts_node_child_by_field_name( n, "type", 4 ), src ), + pushRawBind( fileId, lang, paramDeclaratorVarName( loopDeclarator, src ), writtenTypeOf( fieldChild( n, NodeField::Type ), src ), BindSite{ ts_node_start_byte( n ), 0u, 0u }, LocalBindKind::ParamType, binds ); return; } @@ -1085,7 +1085,7 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file // a catch parameter is a local of its HANDLER block (iteration 3, the noted 3b gap) — its // parameter_list is a direct field; a definition's sits behind the declarator chain // (fnDefParameterList above), which is what keeps prototypes and fn-pointer TYPE params out. - const TSNode params = isCatch ? ts_node_child_by_field_name( n, "parameters", 10 ) : fnDefParameterList( n ); + const TSNode params = isCatch ? fieldChild( n, NodeField::Parameters ) : fnDefParameterList( n ); if( !ts_node_is_null( params ) ) { emitShadowParamDecls( params, fileId, lang, src, bodySite, binds ); @@ -1115,7 +1115,7 @@ inline void captureFnBindDecl( TSNode n, std::uint32_t fileId, Lang lang, std::s std::vector& fnPos, std::vector& fnUnk, std::vector& pending ) { - const TSNode typeNode = ts_node_child_by_field_name( n, "type", 4 ); + const TSNode typeNode = fieldChild( n, NodeField::Type ); std::string writtenType; const bool concrete = concreteWrittenType( typeNode, src, writtenType ); const std::uint32_t cc = ts_node_child_count( n ); @@ -1131,8 +1131,8 @@ inline void captureFnBindDecl( TSNode n, std::uint32_t fileId, Lang lang, std::s { continue; // no initializer → no binding fact here (a later assignment carries its own) } - const auto [ var, sawFnDecl, sawPtrDecl, sawRef ] = fnDeclaratorShape( ts_node_child_by_field_name( c, "declarator", 10 ), src ); - const TSNode valueNode = ts_node_child_by_field_name( c, "value", 5 ); + const auto [ var, sawFnDecl, sawPtrDecl, sawRef ] = fnDeclaratorShape( fieldChild( c, NodeField::Declarator ), src ); + const TSNode valueNode = fieldChild( c, NodeField::Value ); if( sawRef ) { // A5 escape guard: `H& r = fn;` / `auto& r = fn;` ALIASES fn — a write through r retargets fn @@ -1175,7 +1175,7 @@ inline void captureFnBindEscape( TSNode n, std::string_view src, std::vector& fnPos, std::vector& fnUnk, std::vector& pending ) { - const TSNode lhs = ts_node_child_by_field_name( n, "left", 4 ); - const TSNode rhs = ts_node_child_by_field_name( n, "right", 5 ); + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); if( !ts_node_is_null( lhs ) && kindIs( ts_node_type( lhs ), "identifier" ) ) { const std::uint32_t a = ts_node_start_byte( lhs ), b = ts_node_end_byte( lhs ); @@ -1329,7 +1329,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // C++/ObjC: `Foo x;` · `Foo* x;` · `Foo x = Foo();` · `auto x = Foo();` if( ( lang == Lang::Cpp || lang == Lang::ObjC ) && kindIs( t, "declaration" ) ) { - const TSNode typeNode = ts_node_child_by_field_name( n, "type", 4 ); + const TSNode typeNode = fieldChild( n, NodeField::Type ); std::string written = writtenTypeOf( typeNode, src ); // A5 fix round: the declared names shadow within their enclosing block (or, for a control-statement // header declaration, that whole statement) — one parent walk per declaration node, shared by every @@ -1356,8 +1356,8 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // exists and shadows). if( kindIs( ct, "init_declarator" ) ) { - const TSNode declarator = ts_node_child_by_field_name( c, "declarator", 10 ); - std::string type = written.empty() ? ctorTypeOf( ts_node_child_by_field_name( c, "value", 5 ), src ) : written; + const TSNode declarator = fieldChild( c, NodeField::Declarator ); + std::string type = written.empty() ? ctorTypeOf( fieldChild( c, NodeField::Value ), src ) : written; emitDeclBinds( fileId, lang, declarator, src, std::move( type ), BindSite{ ts_node_start_byte( n ), shadowSpanStart( scope, declarator ), scope.end }, binds ); } @@ -1371,8 +1371,8 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // C++ `x = Foo();` (re-assignment to a constructor) — assignment_expression inside an expression_statement. else if( ( lang == Lang::Cpp || lang == Lang::ObjC ) && kindIs( t, "assignment_expression" ) ) { - const TSNode lhs = ts_node_child_by_field_name( n, "left", 4 ); - const TSNode rhs = ts_node_child_by_field_name( n, "right", 5 ); + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); if( !ts_node_is_null( lhs ) && kindIs( ts_node_type( lhs ), "identifier" ) ) { const std::uint32_t a = ts_node_start_byte( lhs ), b = ts_node_end_byte( lhs ); @@ -1385,8 +1385,8 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // Python `x = Foo()` — assignment with a bare-identifier LHS and a constructor-call RHS. else if( lang == Lang::Python && kindIs( t, "assignment" ) ) { - const TSNode lhs = ts_node_child_by_field_name( n, "left", 4 ); - const TSNode rhs = ts_node_child_by_field_name( n, "right", 5 ); + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); if( !ts_node_is_null( lhs ) && kindIs( ts_node_type( lhs ), "identifier" ) ) { const std::uint32_t a = ts_node_start_byte( lhs ), b = ts_node_end_byte( lhs ); @@ -1396,7 +1396,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) std::string type; if( !ts_node_is_null( rhs ) && kindIs( ts_node_type( rhs ), "call" ) ) { - const TSNode fn = ts_node_child_by_field_name( rhs, "function", 8 ); + const TSNode fn = fieldChild( rhs, NodeField::Function ); if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "identifier" ) ) { const std::uint32_t fa = ts_node_start_byte( fn ), fb = ts_node_end_byte( fn ); @@ -1413,7 +1413,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // TypeScript `const x = new Foo();` · `let y: Bar = ...;` — variable_declarator. else if( lang == Lang::TypeScript && kindIs( t, "variable_declarator" ) ) { - const TSNode nameNode = ts_node_child_by_field_name( n, "name", 4 ); + const TSNode nameNode = fieldChild( n, NodeField::Name ); if( !ts_node_is_null( nameNode ) && kindIs( ts_node_type( nameNode ), "identifier" ) ) { const std::uint32_t a = ts_node_start_byte( nameNode ), b = ts_node_end_byte( nameNode ); @@ -1421,7 +1421,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) { // prefer the `: Type` annotation; else infer from a `new Foo()` / `Foo()` initializer. std::string type; - const TSNode ann = ts_node_child_by_field_name( n, "type", 4 ); // type_annotation + const TSNode ann = fieldChild( n, NodeField::Type ); // type_annotation if( !ts_node_is_null( ann ) ) { const std::uint32_t cc = ts_node_child_count( ann ); @@ -1435,7 +1435,7 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) } if( type.empty() ) { - type = ctorTypeOf( ts_node_child_by_field_name( n, "value", 5 ), src ); + type = ctorTypeOf( fieldChild( n, NodeField::Value ), src ); } emitBind( fileId, lang, src.substr( a, b - a ), std::move( type ), ts_node_start_byte( n ), binds ); } diff --git a/src/ingest_crawl.h b/src/ingest_crawl.h index 68f010c87..3c48cddcb 100644 --- a/src/ingest_crawl.h +++ b/src/ingest_crawl.h @@ -1589,6 +1589,39 @@ TSQuery* compileQueryStandalone( const LangEntry& le ) return q; } +// ---- the [grammar][field] TSFieldId table, filled ONCE per grammar (src/infra/fieldid.h) ---- +// Same shape and same invariant as the compiled-query cache above: written single-threaded before any +// parse worker exists, read lock-free per AST node afterwards. It warms EVERY grammar the table can +// name rather than the crawl's miss set, for two reasons. First, the miss set is empty on a fully-warm +// run, and the AST walks that read this table are not: --slice, --lint and the preprocessor reader parse +// outside the tags prewarm entirely. Second, the cost is a fixed few hundred microseconds — 23 grammars +// x 41 field names, each one linear-scan resolved ONCE — against a per-AST-node saving, so paying it for +// a grammar the run never uses is cheaper than reasoning about which runs need which. +// +// The function-local static is what makes it idempotent and thread-safe at the seam (ingest() can be +// re-entered in a long-lived MCP server); rw::warmFieldIds itself is neither, which is why nothing else +// may call it. Gate: test/fieldidcheck.sh arm E-warm. +// A 65th extension row must be a compile error, not a run that silently keeps the by-name path: the row +// count bounds the DISTINCT grammar count the loop below registers, so this assert bounds the registry. +static_assert( kLangTable.size() <= kFieldIdCapacity, + "kLangTable has more rows than rw::kFieldIdCapacity — raise the capacity in src/infra/fieldid.h" ); + +inline void warmFieldIdTable() +{ + static const bool warmed = []() + { + for( const LangEntry& le : kLangTable ) + { + if( le.grammar != nullptr ) + { + warmFieldIds( le.grammar() ); // nullptr-safe and idempotent; markdown rows have no grammar + } + } + return true; + }(); + (void) warmed; +} + TSQuery* compiledQueryFor( const LangEntry& le ) { if( le.grammar == nullptr ) diff --git a/src/ingest_elixir.h b/src/ingest_elixir.h index 251649283..9f7e56ecf 100644 --- a/src/ingest_elixir.h +++ b/src/ingest_elixir.h @@ -19,7 +19,7 @@ std::string_view elixirTarget( TSNode node, std::string_view src ) noexcept { return {}; } - const TSNode target = ts_node_child_by_field_name( node, "target", 6 ); + const TSNode target = fieldChild( node, NodeField::Target ); if( ts_node_is_null( target ) || std::strcmp( ts_node_type( target ), "identifier" ) != 0 ) { return {}; @@ -85,14 +85,14 @@ TSNode elixirKeywordValue( TSNode node, std::string_view key, std::string_view s for( std::uint32_t pairId = 0; pairId < ts_node_named_child_count( arg ); ++pairId ) { const TSNode pair = ts_node_named_child( arg, pairId ); - auto found = nodeTextOf( ts_node_child_by_field_name( pair, "key", 3 ), src ); + auto found = nodeTextOf( fieldChild( pair, NodeField::Key ), src ); while( !found.empty() && std::isspace( static_cast( found.back() ) ) ) { found.remove_suffix( 1 ); } if( found == key ) { - return ts_node_child_by_field_name( pair, "value", 5 ); + return fieldChild( pair, NodeField::Value ); } } } @@ -121,7 +121,7 @@ std::uint16_t elixirParams( TSNode node ) noexcept TSNode head = elixirFirstArgument( node ); if( !ts_node_is_null( head ) && std::strcmp( ts_node_type( head ), "binary_operator" ) == 0 ) { - head = ts_node_child_by_field_name( head, "left", 4 ); + head = fieldChild( head, NodeField::Left ); } const TSNode args = elixirArguments( head ); const auto count = ts_node_is_null( args ) ? 0u : ts_node_named_child_count( args ); @@ -137,7 +137,7 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { if( elixirTarget( parent, src ) == "quote" - || ( std::strcmp( ts_node_type( parent ), "unary_operator" ) == 0 && nodeFieldText( parent, "operator", 8, src ) == "@" ) ) + || ( std::strcmp( ts_node_type( parent ), "unary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "@" ) ) { return false; } @@ -169,10 +169,10 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: { return false; // the test declaration itself is not a call } - const TSNode callTarget = ts_node_child_by_field_name( role, "target", 6 ); + const TSNode callTarget = fieldChild( role, NodeField::Target ); if( !ts_node_is_null( callTarget ) && std::strcmp( ts_node_type( callTarget ), "dot" ) == 0 ) { - const TSNode receiver = ts_node_child_by_field_name( callTarget, "left", 4 ); + const TSNode receiver = fieldChild( callTarget, NodeField::Left ); if( ts_node_is_null( receiver ) || std::strcmp( ts_node_type( receiver ), "alias" ) != 0 ) { return false; // runtime receiver / anonymous function dispatch cannot name a module @@ -188,9 +188,9 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: bool inDefault = false; for( TSNode parent = ts_node_parent( role ); !ts_node_is_null( parent ); parent = ts_node_parent( parent ) ) { - if( std::strcmp( ts_node_type( parent ), "binary_operator" ) == 0 && nodeFieldText( parent, "operator", 8, src ) == "\\\\" ) + if( std::strcmp( ts_node_type( parent ), "binary_operator" ) == 0 && nodeFieldText( parent, NodeField::Operator, src ) == "\\\\" ) { - const TSNode value = ts_node_child_by_field_name( parent, "right", 5 ); + const TSNode value = fieldChild( parent, NodeField::Right ); inDefault = inDefault || ( !ts_node_is_null( value ) && ts_node_start_byte( role ) >= ts_node_start_byte( value ) && ts_node_end_byte( role ) <= ts_node_end_byte( value ) ); } @@ -201,7 +201,7 @@ bool elixirKeepCapture( TSNode role, TSNode name, bool isDef, SymKind kind, std: TSNode head = elixirFirstArgument( parent ); if( !ts_node_is_null( head ) && std::strcmp( ts_node_type( head ), "binary_operator" ) == 0 ) { - head = ts_node_child_by_field_name( head, "left", 4 ); + head = fieldChild( head, NodeField::Left ); } if( !ts_node_is_null( head ) && ts_node_start_byte( name ) >= ts_node_start_byte( head ) && ts_node_end_byte( name ) <= ts_node_end_byte( head ) ) { diff --git a/src/ingest_jsimports.h b/src/ingest_jsimports.h index 50cdd5a24..227889795 100644 --- a/src/ingest_jsimports.h +++ b/src/ingest_jsimports.h @@ -73,10 +73,10 @@ inline std::vector jsPatternNames( TSNode pattern, std::string_view else if( jsNodeIs( node, "pair_pattern" ) || jsNodeIs( node, "assignment_pattern" ) || jsNodeIs( node, "object_assignment_pattern" ) || jsNodeIs( node, "required_parameter" ) || jsNodeIs( node, "optional_parameter" ) ) { - const char* field = jsNodeIs( node, "pair_pattern" ) ? "value" - : ( jsNodeIs( node, "required_parameter" ) || jsNodeIs( node, "optional_parameter" ) ) ? "pattern" : "left"; - TSNode child = ts_node_child_by_field_name( node, field, std::strlen( field ) ); - if( ts_node_is_null( child ) ) { child = ts_node_child_by_field_name( node, "name", 4 ); } + const NodeField field = jsNodeIs( node, "pair_pattern" ) ? NodeField::Value + : ( jsNodeIs( node, "required_parameter" ) || jsNodeIs( node, "optional_parameter" ) ) ? NodeField::Pattern : NodeField::Left; + TSNode child = fieldChild( node, field ); + if( ts_node_is_null( child ) ) { child = fieldChild( node, NodeField::Name ); } if( !ts_node_is_null( child ) ) { pending.push_back( child ); } } else if( std::strcmp( kind, "formal_parameters" ) == 0 || std::strcmp( kind, "array_pattern" ) == 0 @@ -101,7 +101,7 @@ inline std::vector jsPatternNames( TSNode pattern, std::string_view inline std::vector> jsExportClauseNames( TSNode stmt, std::string_view src ) { std::vector> names; - if( !ts_node_is_null( ts_node_child_by_field_name( stmt, "source", 6 ) ) || jsHasToken( stmt, "type" ) ) + if( !ts_node_is_null( fieldChild( stmt, NodeField::Source ) ) || jsHasToken( stmt, "type" ) ) { return names; } @@ -112,8 +112,8 @@ inline std::vector> jsExportClauseNames( TSN pending.pop_back(); if( jsNodeIs( node, "export_specifier" ) ) { - TSNode local = ts_node_child_by_field_name( node, "name", 4 ); - TSNode alias = ts_node_child_by_field_name( node, "alias", 5 ); + TSNode local = fieldChild( node, NodeField::Name ); + TSNode alias = fieldChild( node, NodeField::Alias ); if( ts_node_is_null( alias ) ) { alias = local; } if( jsNodeIs( local, "identifier" ) && jsNodeIs( alias, "identifier" ) && !jsHasToken( node, "type" ) ) { @@ -141,7 +141,7 @@ inline std::uint32_t jsModuleBindingCount( TSNode root, std::string_view name, s collectChildren( root, cursor.cur, children ); for( TSNode stmt : children ) { - TSNode decl = jsNodeIs( stmt, "export_statement" ) ? ts_node_child_by_field_name( stmt, "declaration", 11 ) : stmt; + TSNode decl = jsNodeIs( stmt, "export_statement" ) ? fieldChild( stmt, NodeField::Declaration ) : stmt; if( ts_node_is_null( decl ) || jsHasToken( stmt, "type" ) ) { continue; } std::vector pending{ decl }; while( !pending.empty() ) @@ -153,12 +153,12 @@ inline std::uint32_t jsModuleBindingCount( TSNode root, std::string_view name, s || jsNodeIs( node, "generator_function_declaration" ) || jsNodeIs( node, "class_declaration" ) || jsNodeIs( node, "abstract_class_declaration" ) || jsNodeIs( node, "enum_declaration" ) ) { - binding = ts_node_child_by_field_name( node, "name", 4 ); + binding = fieldChild( node, NodeField::Name ); } else if( jsNodeIs( node, "import_specifier" ) && !jsHasToken( node, "type" ) ) { - binding = ts_node_child_by_field_name( node, "alias", 5 ); - if( ts_node_is_null( binding ) ) { binding = ts_node_child_by_field_name( node, "name", 4 ); } + binding = fieldChild( node, NodeField::Alias ); + if( ts_node_is_null( binding ) ) { binding = fieldChild( node, NodeField::Name ); } } else if( jsNodeIs( node, "identifier" ) ) { binding = node; } else if( jsNodeIs( node, "lexical_declaration" ) || jsNodeIs( node, "variable_declaration" ) @@ -216,7 +216,7 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, { if( jsNodeIs( stmt, "import_statement" ) ) { - TSNode source = ts_node_child_by_field_name( stmt, "source", 6 ); + TSNode source = fieldChild( stmt, NodeField::Source ); if( ts_node_is_null( source ) ) { continue; } const std::string module = importSpecifierText( source, src ); std::vector pending{ stmt }; @@ -227,8 +227,8 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, const bool isDefault = jsNodeIs( node, "identifier" ) && jsNodeIs( ts_node_parent( node ), "import_clause" ); if( isDefault || jsNodeIs( node, "import_specifier" ) ) { - TSNode name = isDefault ? node : ts_node_child_by_field_name( node, "name", 4 ); - TSNode alias = isDefault ? node : ts_node_child_by_field_name( node, "alias", 5 ); + TSNode name = isDefault ? node : fieldChild( node, NodeField::Name ); + TSNode alias = isDefault ? node : fieldChild( node, NodeField::Alias ); if( ts_node_is_null( alias ) ) { alias = name; } if( !jsNodeIs( alias, "identifier" ) ) { continue; } std::string local( pattern::nodeText( alias, src ) ); @@ -251,12 +251,12 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, } else if( jsNodeIs( stmt, "export_statement" ) ) { - TSNode decl = ts_node_child_by_field_name( stmt, "declaration", 11 ); + TSNode decl = fieldChild( stmt, NodeField::Declaration ); if( jsHasToken( stmt, "default" ) ) { - TSNode value = ts_node_child_by_field_name( stmt, "value", 5 ); + TSNode value = fieldChild( stmt, NodeField::Value ); TSNode name{}; - if( !ts_node_is_null( decl ) ) { name = ts_node_child_by_field_name( decl, "name", 4 ); } + if( !ts_node_is_null( decl ) ) { name = fieldChild( decl, NodeField::Name ); } record( stmt, LocalBindKind::JsExport, "default", root ); if( jsNodeIs( value, "identifier" ) && jsModuleBindingCount( root, pattern::nodeText( value, src ), src ) == 1 ) { @@ -292,7 +292,7 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, } continue; } - TSNode name = ts_node_child_by_field_name( decl, "name", 4 ); + TSNode name = fieldChild( decl, NodeField::Name ); if( jsNodeIs( decl, "function_declaration" ) || jsNodeIs( decl, "generator_function_declaration" ) || jsNodeIs( decl, "class_declaration" ) || jsNodeIs( decl, "abstract_class_declaration" ) ) { @@ -306,9 +306,9 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, for( TSNode variable : declarators ) { if( !jsNodeIs( variable, "variable_declarator" ) ) { continue; } - TSNode value = ts_node_child_by_field_name( variable, "value", 5 ); + TSNode value = fieldChild( variable, NodeField::Value ); if( !jsNodeIs( value, "arrow_function" ) && !jsNodeIs( value, "function_expression" ) ) { continue; } - TSNode binding = ts_node_child_by_field_name( variable, "name", 4 ); + TSNode binding = fieldChild( variable, NodeField::Name ); if( jsNodeIs( binding, "identifier" ) ) { record( stmt, LocalBindKind::JsExport, std::string( pattern::nodeText( binding, src ) ), decl ); @@ -330,18 +330,18 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, TSNode scope{}; if( jsFunctionScope( node ) ) { - binding = ts_node_child_by_field_name( node, "parameters", 10 ); - if( ts_node_is_null( binding ) ) { binding = ts_node_child_by_field_name( node, "parameter", 9 ); } + binding = fieldChild( node, NodeField::Parameters ); + if( ts_node_is_null( binding ) ) { binding = fieldChild( node, NodeField::Parameter ); } scope = node; } else if( jsNodeIs( node, "catch_clause" ) ) { - binding = ts_node_child_by_field_name( node, "parameter", 9 ); + binding = fieldChild( node, NodeField::Parameter ); scope = node; } else if( jsNodeIs( node, "for_in_statement" ) ) { - binding = ts_node_child_by_field_name( node, "left", 4 ); + binding = fieldChild( node, NodeField::Left ); scope = node; if( jsHasToken( node, "var" ) ) { @@ -353,7 +353,7 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, } else if( jsNodeIs( node, "variable_declarator" ) ) { - binding = ts_node_child_by_field_name( node, "name", 4 ); + binding = fieldChild( node, NodeField::Name ); const bool isVar = jsNodeIs( ts_node_parent( node ), "variable_declaration" ); for( scope = ts_node_parent( node ); !ts_node_is_null( scope ); scope = ts_node_parent( scope ) ) { @@ -365,7 +365,7 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, if( ( jsNodeIs( node, "variable_declarator" ) || jsNodeIs( node, "for_in_statement" ) ) && !ts_node_is_null( scope ) && jsFunctionScope( scope ) ) { - scope = ts_node_child_by_field_name( scope, "body", 4 ); + scope = fieldChild( scope, NodeField::Body ); } for( std::string name : jsPatternNames( binding, src ) ) { @@ -376,7 +376,7 @@ inline void captureJsImportFacts( TSNode root, Lang lang, std::uint32_t fileId, || jsNodeIs( node, "function_expression" ) || jsNodeIs( node, "generator_function" ) || jsNodeIs( node, "class" ) || jsNodeIs( node, "abstract_class_declaration" ) ) { - TSNode nameNode = ts_node_child_by_field_name( node, "name", 4 ); + TSNode nameNode = fieldChild( node, NodeField::Name ); if( !ts_node_is_null( nameNode ) ) { std::string name( pattern::nodeText( nameNode, src ) ); diff --git a/src/ingest_metrics.h b/src/ingest_metrics.h index 5b2de028f..5b1815818 100644 --- a/src/ingest_metrics.h +++ b/src/ingest_metrics.h @@ -123,8 +123,8 @@ inline bool cc_isNestingOnly( const char* t ) noexcept // raises nesting, scor // preprocessor include readers did), so it lives once, here — hoisted above the first consumer rather // than sitting halfway down the file where three helpers ahead of it could not reach it. // -// A FIELD read is the common case: nodeFieldText( n, "operator", 8, src ) is the whole of what most -// callers want, and ts_node_child_by_field_name's length argument is the one thing easy to get wrong. +// A FIELD read is the common case: nodeFieldText( n, NodeField::Operator, src ) is the whole of what most +// callers want, and infra/fieldid.h is what keeps the field's id out of the per-node path (see it for why). inline std::string_view nodeTextOf( TSNode node, std::string_view src ) noexcept { if( ts_node_is_null( node ) ) @@ -135,9 +135,9 @@ inline std::string_view nodeTextOf( TSNode node, std::string_view src ) noexcept return ( a <= b && b <= src.size() ) ? src.substr( a, b - a ) : std::string_view{}; } -inline std::string_view nodeFieldText( TSNode node, const char* field, std::uint32_t fieldLen, std::string_view src ) noexcept +inline std::string_view nodeFieldText( TSNode node, NodeField field, std::string_view src ) noexcept { - return nodeTextOf( ts_node_child_by_field_name( node, field, fieldLen ), src ); + return nodeTextOf( fieldChild( node, field ), src ); } // The written spelling of a node's `operator:` field, or "" when it has none / the span is out of range. @@ -147,7 +147,7 @@ inline std::string_view nodeFieldText( TSNode node, const char* field, std::uint // hand-copied spans — a duplication --quality-delta scored the moment the second one grew a case. inline std::string_view cc_operatorText( TSNode n, std::string_view src ) noexcept { - return nodeFieldText( n, "operator", 8, src ); + return nodeFieldText( n, NodeField::Operator, src ); } // the boolean-operator spelling of a node, or "" if it isn't one (&&/|| for C-family, and/or for Python) @@ -1049,7 +1049,7 @@ inline void cc_walk( TSNode start, std::uint32_t startNesting, std::string_view // cyclomatic (flat decision count) accumulated in the SAME DFS as cognitive — one walk, both metrics. // Elixir controls are ordinary calls whose target text supplies the keyword. - const auto elixirKeyword = lang == Lang::Elixir ? nodeFieldText( n, "target", 6, src ) : std::string_view{}; + const auto elixirKeyword = lang == Lang::Elixir ? nodeFieldText( n, NodeField::Target, src ) : std::string_view{}; if( elixirKeyword == "quote" ) { continue; // quoted AST is not executed control flow @@ -1554,7 +1554,7 @@ inline std::pair callArity( TSNode nameNode, Lang lang, std } // find the argument container: the `arguments` field, else the first child of a known list type. - TSNode args = ts_node_child_by_field_name( call, "arguments", 9 ); + TSNode args = fieldChild( call, NodeField::Arguments ); if( ts_node_is_null( args ) ) { const std::uint32_t cc = ts_node_child_count( call ); diff --git a/src/ingest_names.h b/src/ingest_names.h index d7ebdc99f..3b4452934 100644 --- a/src/ingest_names.h +++ b/src/ingest_names.h @@ -223,7 +223,7 @@ inline TSNode innermostQualifiedName( TSNode n ) noexcept { break; } - const TSNode inner = ts_node_child_by_field_name( n, "name", 4 ); + const TSNode inner = fieldChild( n, NodeField::Name ); if( ts_node_is_null( inner ) ) { break; @@ -296,7 +296,7 @@ inline std::string qualifierOf( TSNode nameNode, std::string_view src ) { return {}; // error-recovery artefact, not a written qualification } - const TSNode scope = ts_node_child_by_field_name( parent, "scope", 5 ); + const TSNode scope = fieldChild( parent, NodeField::Scope ); if( ts_node_is_null( scope ) ) { return {}; @@ -379,7 +379,7 @@ inline std::string rustEnclosingScopeOf( TSNode node, std::string_view src, bool // impl carries the implementor under `type:`; trait/mod carry their own `name:`. Anonymous/ill-formed // (empty text) yields "" — no usable scope — which is the same degrade as "no owner above". - const TSNode owner = isImpl ? ts_node_child_by_field_name( p, "type", 4 ) : ts_node_child_by_field_name( p, "name", 4 ); + const TSNode owner = isImpl ? fieldChild( p, NodeField::Type ) : fieldChild( p, NodeField::Name ); // V3 L-1: a container is not its OWN scope. `mod util { … }`'s definition node IS that `name:` child, so // the first ancestor found is the module itself and `util` would be published as `util::util` (likewise // `Shape::Shape`) — a self-scope in the canonical-id space, which is what ids are keyed on. Keep walking @@ -408,7 +408,7 @@ inline std::string rustQualifierOf( TSNode nameNode, std::string_view src ) return {}; } - std::string qualifier = rustPathSegment( nodeTextOf( ts_node_child_by_field_name( parent, "path", 4 ), src ) ); + std::string qualifier = rustPathSegment( nodeTextOf( fieldChild( parent, NodeField::Path ), src ) ); // `Self::helper()` — resolve `Self` to the ENCLOSING impl/trait type at EXTRACTION time, so the ref keys // the same canonical entry the def side wrote (`Widget::helper`). Precedent: captureRustImpls already // reads an impl header's `type:` for inherit refs. Falls back to bare-name when there is no impl above. @@ -432,7 +432,7 @@ inline std::string enclosingScopeOf( TSNode node, std::string_view src ) || kindIs( t, "namespace_definition" ) || kindIs( t, "class_definition" ); if( scopeOwner ) { - const TSNode nm = ts_node_child_by_field_name( p, "name", 4 ); + const TSNode nm = fieldChild( p, NodeField::Name ); if( ts_node_is_null( nm ) ) { return {}; // anonymous → no usable scope @@ -466,7 +466,7 @@ inline std::string rubyEnclosingScopeOf( TSNode nameNode, std::string_view src ) { continue; } - TSNode nm = ts_node_child_by_field_name( p, "name", 4 ); + TSNode nm = fieldChild( p, NodeField::Name ); if( ts_node_is_null( nm ) ) { return {}; // anonymous → no usable scope (the grammar always names these; guard, don't assert) @@ -477,7 +477,7 @@ inline std::string rubyEnclosingScopeOf( TSNode nameNode, std::string_view src ) } if( kindIs( ts_node_type( nm ), "scope_resolution" ) ) { - const TSNode last = ts_node_child_by_field_name( nm, "name", 4 ); + const TSNode last = fieldChild( nm, NodeField::Name ); if( !ts_node_is_null( last ) ) { nm = last; @@ -511,7 +511,7 @@ inline bool rubyCallIsAssignmentTarget( TSNode nameNode ) noexcept { return false; } - const TSNode left = ts_node_child_by_field_name( assign, "left", 4 ); + const TSNode left = fieldChild( assign, NodeField::Left ); return !ts_node_is_null( left ) && ts_node_eq( left, call ); } @@ -680,7 +680,7 @@ inline bool csharpNodeCarriesTestAttr( TSNode n, std::string_view src ) noexcept { const TSNode attr = ts_node_child( list, ai ); if( kindIs( ts_node_type( attr ), "attribute" ) - && csharpAttrIsTestMarker( nodeTextOf( ts_node_child_by_field_name( attr, "name", 4 ), src ) ) ) + && csharpAttrIsTestMarker( nodeTextOf( fieldChild( attr, NodeField::Name ), src ) ) ) { return true; } @@ -702,7 +702,7 @@ inline bool pythonInFileTestScope( TSNode defNode, std::string_view src ) noexce { continue; } - if( pyTestClassName( nodeTextOf( ts_node_child_by_field_name( n, "name", 4 ), src ) ) ) + if( pyTestClassName( nodeTextOf( fieldChild( n, NodeField::Name ), src ) ) ) { return true; // a member of a Test* class, at any nesting depth } @@ -712,7 +712,7 @@ inline bool pythonInFileTestScope( TSNode defNode, std::string_view src ) noexce { return false; } - return nodeTextOf( ts_node_child_by_field_name( defNode, "name", 4 ), src ).rfind( "test_", 0 ) == 0; + return nodeTextOf( fieldChild( defNode, NodeField::Name ), src ).rfind( "test_", 0 ) == 0; } // Is `pred` true of `node` itself or of any of its ancestors? Three of the four in-file test rules ask @@ -756,7 +756,7 @@ inline bool jsInFileTestScope( TSNode defNode, std::string_view src ) noexcept { return false; } - const std::string_view callee = nodeTextOf( ts_node_child_by_field_name( n, "function", 8 ), src ); + const std::string_view callee = nodeTextOf( fieldChild( n, NodeField::Function ), src ); return callee == "describe" || callee == "it" || callee == "test"; } ); } @@ -857,7 +857,7 @@ inline TestMacroBlockParts testMacroBlockPartsOf( TSNode exprStmtNode, std::stri { return {}; } - const std::string_view callee = nodeTextOf( ts_node_child_by_field_name( call, "function", 8 ), src ); + const std::string_view callee = nodeTextOf( fieldChild( call, NodeField::Function ), src ); bool isKnownMacro = false; for( const std::string_view macroName : kTestBlockMacroNames ) { @@ -873,7 +873,7 @@ inline TestMacroBlockParts testMacroBlockPartsOf( TSNode exprStmtNode, std::stri } // the FIRST string literal among the arguments is the title - const TSNode args = ts_node_child_by_field_name( call, "arguments", 9 ); + const TSNode args = fieldChild( call, NodeField::Arguments ); const std::uint32_t argCount = ts_node_is_null( args ) ? 0u : ts_node_named_child_count( args ); for( std::uint32_t argIx = 0; argIx < argCount; ++argIx ) { @@ -1091,7 +1091,7 @@ inline bool fieldCaptureKept( Lang lang, TSNode nameNode, TSNode roleNode, std:: { return false; } - const TSNode object = ts_node_child_by_field_name( parent, "object", 6 ); + const TSNode object = fieldChild( parent, NodeField::Object ); if( ts_node_is_null( object ) || !kindIs( ts_node_type( object ), "identifier" ) || nodeTextOf( object, src ) != "self" ) { return false; // `obj.x = …` / `cls.x = …` — not an instance attribute of the enclosing class @@ -1282,7 +1282,7 @@ inline bool isCjsExportTarget( TSNode nameNode, std::string_view src ) noexcept { return false; } - const TSNode obj = ts_node_child_by_field_name( member, "object", 6 ); + const TSNode obj = fieldChild( member, NodeField::Object ); if( ts_node_is_null( obj ) ) { return false; @@ -1294,10 +1294,10 @@ inline bool isCjsExportTarget( TSNode nameNode, std::string_view src ) noexcept } if( kindIs( objType, "member_expression" ) ) { - const TSNode oo = ts_node_child_by_field_name( obj, "object", 6 ); + const TSNode oo = fieldChild( obj, NodeField::Object ); return kindIs( ts_node_type( oo ), "identifier" ) && nodeTextOf( oo, src ) == "module" - && nodeTextOf( ts_node_child_by_field_name( obj, "property", 8 ), src ) == "exports"; + && nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "exports"; } return false; } @@ -1312,12 +1312,12 @@ inline bool isPrototypeMemberTarget( TSNode nameNode, std::string_view src ) noe { return false; } - const TSNode obj = ts_node_child_by_field_name( member, "object", 6 ); + const TSNode obj = fieldChild( member, NodeField::Object ); if( ts_node_is_null( obj ) || !kindIs( ts_node_type( obj ), "member_expression" ) ) { return false; } - return nodeTextOf( ts_node_child_by_field_name( obj, "property", 8 ), src ) == "prototype"; + return nodeTextOf( fieldChild( obj, NodeField::Property ), src ) == "prototype"; } // Python shape round (test/pyshapecheck.sh): `NAME = value` in a class body is a definition only when @@ -1337,7 +1337,7 @@ inline bool isPyEnumMemberTarget( TSNode nameNode, std::string_view src ) noexce { return false; } - const TSNode bases = ts_node_child_by_field_name( cls, "superclasses", 12 ); + const TSNode bases = fieldChild( cls, NodeField::Superclasses ); if( ts_node_is_null( bases ) ) { return false; @@ -1348,7 +1348,7 @@ inline bool isPyEnumMemberTarget( TSNode nameNode, std::string_view src ) noexce TSNode base = ts_node_named_child( bases, baseIndex ); if( kindIs( ts_node_type( base ), "attribute" ) ) // models.TextChoices → TextChoices { - base = ts_node_child_by_field_name( base, "attribute", 9 ); + base = fieldChild( base, NodeField::Attribute ); if( ts_node_is_null( base ) ) { continue; diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 9055514dc..457cee5b8 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -95,7 +95,7 @@ bool macroBodyKeyword( std::string_view w ) noexcept // the `value:` (preproc_arg) child of a preproc_function_def / preproc_def; null node if absent. TSNode preprocValueNode( TSNode defineNode ) noexcept { - return ts_node_child_by_field_name( defineNode, "value", 5 ); + return fieldChild( defineNode, NodeField::Value ); } // the def's body node: the `body:` field for every function/class grammar, and — macro-edges round — a @@ -105,10 +105,10 @@ TSNode preprocValueNode( TSNode defineNode ) noexcept // forward decl. Kept out of captureTagsFacts (the file's densest dispatch point) behind one call. TSNode defBodyNodeOf( TSNode roleNode, SymKind kind ) noexcept { - TSNode body = ts_node_child_by_field_name( roleNode, "body", 4 ); + TSNode body = fieldChild( roleNode, NodeField::Body ); if( ts_node_is_null( body ) && kind == SymKind::Macro ) { - body = ts_node_child_by_field_name( roleNode, "value", 5 ); + body = fieldChild( roleNode, NodeField::Value ); } return body; } @@ -186,7 +186,7 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, // the macro's own name (self-reference never expands) + its parameter names (a param used call-shaped // is the ARGUMENT's business, not a body call — `#define CALL(f) f()` has no resolvable callee here). std::string macroName; - if( const TSNode nameNode = ts_node_child_by_field_name( defineNode, "name", 4 ); !ts_node_is_null( nameNode ) ) + if( const TSNode nameNode = fieldChild( defineNode, NodeField::Name ); !ts_node_is_null( nameNode ) ) { const uint32_t na = ts_node_start_byte( nameNode ); const uint32_t nb = ts_node_end_byte( nameNode ); @@ -196,7 +196,7 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, } } std::vector params; - if( const TSNode paramsNode = ts_node_child_by_field_name( defineNode, "parameters", 10 ); !ts_node_is_null( paramsNode ) ) + if( const TSNode paramsNode = fieldChild( defineNode, NodeField::Parameters ); !ts_node_is_null( paramsNode ) ) { const uint32_t pc = ts_node_child_count( paramsNode ); for( uint32_t i = 0; i < pc; ++i ) @@ -387,8 +387,8 @@ void rustImplVisitNode( RustImplCtx& cx, TSNode node, const char* t ) { return; } - const TSNode traitNode = ts_node_child_by_field_name( node, "trait", 5 ); - const TSNode typeNode = ts_node_child_by_field_name( node, "type", 4 ); + const TSNode traitNode = fieldChild( node, NodeField::Trait ); + const TSNode typeNode = fieldChild( node, NodeField::Type ); if( ts_node_is_null( traitNode ) || ts_node_is_null( typeNode ) ) { return; @@ -451,7 +451,7 @@ void captureFields( TSNode classNode, std::uint32_t fileId, Lang lang, std::stri // type_identifier — a plain class name (SpherePool) // type_descriptor — a reference/pointer type containing a type_identifier // We consider type_identifier directly under type= as the declared type. - const TSNode typeNode = ts_node_child_by_field_name( fdecl, "type", 4 ); + const TSNode typeNode = fieldChild( fdecl, NodeField::Type ); if( ts_node_is_null( typeNode ) ) { continue; @@ -516,7 +516,7 @@ void captureFields( TSNode classNode, std::uint32_t fileId, Lang lang, std::stri // field_identifier — plain value field: `SpherePool m_pool;` // reference_declarator > field_identifier — reference field: `SoundEngine& m_sound;` // pointer_declarator > field_identifier — pointer field: `Foo* m_foo;` - const TSNode decl = ts_node_child_by_field_name( fdecl, "declarator", 10 ); + const TSNode decl = fieldChild( fdecl, NodeField::Declarator ); if( ts_node_is_null( decl ) ) { continue; @@ -641,7 +641,7 @@ inline std::string importSpecifierText( TSNode node, std::string_view src ) // for the first one. inline std::string csharpUsingTarget( TSNode usingNode, std::string_view src ) { - if( const TSNode aliasType = ts_node_child_by_field_name( usingNode, "type", 4 ); !ts_node_is_null( aliasType ) ) + if( const TSNode aliasType = fieldChild( usingNode, NodeField::Type ); !ts_node_is_null( aliasType ) ) { return importSpecifierText( aliasType, src ); } @@ -734,12 +734,12 @@ inline std::string phpUseTarget( TSNode useNode, std::string_view src ) // unresolvable include: a floor, never a wrong answer. inline std::string jsModuleLoadTarget( TSNode n, std::string_view src ) { - const std::string_view callee = nodeFieldText( n, "function", 8, src ); + const std::string_view callee = nodeFieldText( n, NodeField::Function, src ); if( callee != "require" && callee != "import" ) { return {}; } - const TSNode ar = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode ar = fieldChild( n, NodeField::Arguments ); if( ts_node_is_null( ar ) ) { return {}; @@ -794,8 +794,10 @@ std::string includePathOf( std::string_view spelling, bool& isAngleOut ) // plus its own null-and-bounds ladder. Empty when the node carries no readable path field. inline std::string preprocIncludeTarget( TSNode n, std::string_view src, bool& isAngleOut ) { - const std::string_view spelling = nodeFieldText( n, "path", 4, src ); - return spelling.empty() ? std::string{} : includePathOf( spelling, isAngleOut ); + // No empty-guard: includePathOf( "" ) already returns "" through its own size < 2 arm, and leaves + // isAngleOut alone doing it. The guard that used to stand here was dead, and --quality-delta found it + // the way dead code is usually found — as a duplication, once the two call sites normalised alike. + return includePathOf( nodeFieldText( n, NodeField::Path, src ), isAngleOut ); } // The `#import "x.h"` spelling under the C/C++ grammar. `#import` is `#include` + include-once, so it @@ -808,12 +810,12 @@ inline std::string preprocIncludeTarget( TSNode n, std::string_view src, bool& i // beyond #import. inline std::string preprocImportTarget( TSNode n, std::string_view src, bool& isAngleOut ) { - if( nodeFieldText( n, "directive", 9, src ) != "#import" ) + if( nodeFieldText( n, NodeField::Directive, src ) != "#import" ) { return {}; } - const std::string_view spelling = nodeFieldText( n, "argument", 8, src ); // preproc_arg: runs to end-of-line - return spelling.empty() ? std::string{} : includePathOf( spelling, isAngleOut ); // the closing delimiter ends the path + // preproc_arg runs to end-of-line, so the CLOSING delimiter ends the path — includePathOf's job. + return includePathOf( nodeFieldText( n, NodeField::Argument, src ), isAngleOut ); } // ─── kParserVer 81: the four languages that had no directive branch at all ─────────────────────────── @@ -877,12 +879,12 @@ inline std::string bashWordText( TSNode arg, std::string_view src ) // positional parameters to the sourced script, they are not further files. inline std::string bashSourceTarget( TSNode n, std::string_view src ) { - const std::string_view name = nodeFieldText( n, "name", 4, src ); + const std::string_view name = nodeFieldText( n, NodeField::Name, src ); if( name != "source" && name != "." ) { return {}; } - return bashWordText( ts_node_child_by_field_name( n, "argument", 8 ), src ); + return bashWordText( fieldChild( n, NodeField::Argument ), src ); } // The first STRING-literal argument of a call-shaped node, read through the grammar: `arguments`/ @@ -920,12 +922,12 @@ inline std::string firstStringArgText( TSNode args, std::string_view src ) // is somebody's own method, not the loader. inline std::string luaRequireTarget( TSNode n, std::string_view src ) { - const TSNode name = ts_node_child_by_field_name( n, "name", 4 ); + const TSNode name = fieldChild( n, NodeField::Name ); if( ts_node_is_null( name ) || !kindIs( ts_node_type( name ), "identifier" ) || nodeTextOf( name, src ) != "require" ) { return {}; } - return firstStringArgText( ts_node_child_by_field_name( n, "arguments", 9 ), src ); + return firstStringArgText( fieldChild( n, NodeField::Arguments ), src ); } // Ruby `require_relative "x"` / `require "lib/x"` / `load "x.rb"` — a `call` whose `method:` is one of @@ -944,11 +946,11 @@ inline std::string luaRequireTarget( TSNode n, std::string_view src ) // mis-resolve: unique-or-degrade means a wrong file-relative guess simply finds nothing. inline std::string rubyRequireTarget( TSNode n, std::string_view src ) { - if( !ts_node_is_null( ts_node_child_by_field_name( n, "receiver", 8 ) ) ) + if( !ts_node_is_null( fieldChild( n, NodeField::Receiver ) ) ) { return {}; } - const TSNode method = ts_node_child_by_field_name( n, "method", 6 ); + const TSNode method = fieldChild( n, NodeField::Method ); if( ts_node_is_null( method ) || !kindIs( ts_node_type( method ), "identifier" ) ) { return {}; @@ -958,7 +960,7 @@ inline std::string rubyRequireTarget( TSNode n, std::string_view src ) { return {}; } - std::string spec = firstStringArgText( ts_node_child_by_field_name( n, "arguments", 9 ), src ); + std::string spec = firstStringArgText( fieldChild( n, NodeField::Arguments ), src ); if( spec.empty() ) { return {}; @@ -989,7 +991,7 @@ inline std::string rubyRequireTarget( TSNode n, std::string_view src ) // and it is how Ruby spells a constant whose whole definition is its existence — so it defines. Recorded on the ConstOpen captureIncludes emits; read by resolve.h::buildRubyConstantIndex (test/rubyconstcheck.sh, namespace arms). inline bool rubyNamespaceOnly( TSNode defNode ) noexcept { - const TSNode body = ts_node_child_by_field_name( defNode, "body", 4 ); + const TSNode body = fieldChild( defNode, NodeField::Body ); if( ts_node_is_null( body ) ) { return false; // an empty open defines its constant @@ -1047,7 +1049,7 @@ inline std::string rubySuperclassTarget( TSNode superclassNode, std::string_view inline std::string rubyAutoloadTarget( TSNode n, std::string_view src, bool& symbolic ) { symbolic = false; - const TSNode args = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode args = fieldChild( n, NodeField::Arguments ); if( ts_node_is_null( args ) || ts_node_named_child_count( args ) == 0 ) { return {}; @@ -1078,11 +1080,11 @@ inline std::string rubyAutoloadTarget( TSNode n, std::string_view src, bool& sym // `autoload` and the three mixin verbs; `obj.include X` is somebody's own method and reads as nothing. inline std::string_view rubyConstantDirective( TSNode n, std::string_view src ) { - if( !ts_node_is_null( ts_node_child_by_field_name( n, "receiver", 8 ) ) ) + if( !ts_node_is_null( fieldChild( n, NodeField::Receiver ) ) ) { return {}; } - const TSNode method = ts_node_child_by_field_name( n, "method", 6 ); + const TSNode method = fieldChild( n, NodeField::Method ); if( ts_node_is_null( method ) || !kindIs( ts_node_type( method ), "identifier" ) ) { return {}; @@ -1106,7 +1108,7 @@ inline std::vector rubyMixinTargets( TSNode n, std::string_view src { return out; } - const TSNode args = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode args = fieldChild( n, NodeField::Arguments ); if( ts_node_is_null( args ) ) { return out; @@ -1150,8 +1152,8 @@ inline bool rubyIsConstantChain( TSNode n ) noexcept { return false; } - const TSNode name = ts_node_child_by_field_name( n, "name", 4 ); - const TSNode scope = ts_node_child_by_field_name( n, "scope", 5 ); + const TSNode name = fieldChild( n, NodeField::Name ); + const TSNode scope = fieldChild( n, NodeField::Scope ); if( ts_node_is_null( name ) || !kindIs( ts_node_type( name ), "constant" ) ) { return false; @@ -1172,7 +1174,7 @@ inline bool rubyIsConstantChain( TSNode n ) noexcept // rescue class is NOT a receiver: a disclosed floor of this round, stated in the gate's header. inline std::string rubyReceiverTarget( TSNode n, std::string_view src ) { - const TSNode recv = ts_node_child_by_field_name( n, "receiver", 8 ); + const TSNode recv = fieldChild( n, NodeField::Receiver ); if( !rubyIsConstantChain( recv ) ) { return {}; @@ -1196,7 +1198,7 @@ inline std::string rubyReceiverTarget( TSNode n, std::string_view src ) // the name alias does NOT narrow call resolution, exactly as that query's own comment already says. inline std::string elixirDirectiveTarget( TSNode n, std::string_view src ) { - const TSNode target = ts_node_child_by_field_name( n, "target", 6 ); + const TSNode target = fieldChild( n, NodeField::Target ); if( ts_node_is_null( target ) || !kindIs( ts_node_type( target ), "identifier" ) ) { return {}; @@ -1225,7 +1227,7 @@ inline std::string elixirDirectiveTarget( TSNode n, std::string_view src ) inline std::vector elixirAliasGroup( TSNode n, std::string_view src ) { std::vector out; - const TSNode target = ts_node_child_by_field_name( n, "target", 6 ); + const TSNode target = fieldChild( n, NodeField::Target ); if( ts_node_is_null( target ) || !kindIs( ts_node_type( target ), "identifier" ) ) { return out; @@ -1245,8 +1247,8 @@ inline std::vector elixirAliasGroup( TSNode n, std::string_view src { return out; } - const TSNode left = ts_node_child_by_field_name( first, "left", 4 ); - const TSNode right = ts_node_child_by_field_name( first, "right", 5 ); + const TSNode left = fieldChild( first, NodeField::Left ); + const TSNode right = fieldChild( first, NodeField::Right ); if( ts_node_is_null( left ) || ts_node_is_null( right ) || !kindIs( ts_node_type( left ), "alias" ) || !kindIs( ts_node_type( right ), "tuple" ) ) { @@ -1595,13 +1597,13 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src // needs the REAL written specifier, not the clause). Empirically confirmed node shapes: // Python: import_statement name:(dotted_name|aliased_import) → the dotted module `pkg.mod`. // TS/JS: import_statement source:(string) → the quoted specifier `'./x'`. - // ts_node_child_by_field_name returns null for the language that lacks the field, so a single + // A grammar that lacks the field resolves it to id 0, and fieldChild returns null for it, so a single // capture covers both grammars without a per-language branch. - if( const TSNode src_ = ts_node_child_by_field_name( n, "source", 6 ); !ts_node_is_null( src_ ) ) + if( const TSNode src_ = fieldChild( n, NodeField::Source ); !ts_node_is_null( src_ ) ) { target = importSpecifierText( src_, src ); // TS/JS: strip the surrounding quotes } - else if( const TSNode nm = ts_node_child_by_field_name( n, "name", 4 ); !ts_node_is_null( nm ) ) + else if( const TSNode nm = fieldChild( n, NodeField::Name ); !ts_node_is_null( nm ) ) { target = importSpecifierText( nm, src ); // Python: the dotted module head } @@ -1610,7 +1612,7 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src { // module_name:(dotted_name) → `pkg.mod`; module_name:(relative_import) → `.rel` / `..up` (leading // dots preserved so the resolver can resolve relative-to-file). The imported-names clause is dropped. - if( const TSNode mn = ts_node_child_by_field_name( n, "module_name", 11 ); !ts_node_is_null( mn ) ) + if( const TSNode mn = fieldChild( n, NodeField::ModuleName ); !ts_node_is_null( mn ) ) { target = importSpecifierText( mn, src ); } @@ -1663,7 +1665,7 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src { // argument:(scoped_identifier|scoped_use_list|identifier|…) → `crate::a::b`. A brace group // `crate::{a, b}` is kept verbatim; the resolver degrades on it (no unique single-file hit). - if( const TSNode arg = ts_node_child_by_field_name( n, "argument", 8 ); !ts_node_is_null( arg ) ) + if( const TSNode arg = fieldChild( n, NodeField::Argument ); !ts_node_is_null( arg ) ) { target = importSpecifierText( arg, src ); } @@ -1673,9 +1675,9 @@ DirectiveTarget directiveTargetOf( TSNode n, const char* t, std::string_view src // A body-LESS `mod x;` declares module `x` in a sibling file (`x.rs` or `x/mod.rs`); a `mod x { … }` // with a body is INLINE (no file) → skip it. Prefix `mod:` so the Rust resolver applies the // module-file rule, distinct from a bare `use x;`. name:(identifier) → `x`. - if( ts_node_is_null( ts_node_child_by_field_name( n, "body", 4 ) ) ) + if( ts_node_is_null( fieldChild( n, NodeField::Body ) ) ) { - if( const TSNode nm = ts_node_child_by_field_name( n, "name", 4 ); !ts_node_is_null( nm ) ) + if( const TSNode nm = fieldChild( n, NodeField::Name ); !ts_node_is_null( nm ) ) { if( std::string bare = importSpecifierText( nm, src ); !bare.empty() ) { @@ -1754,14 +1756,14 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t std::string target; if( isFrom ) { - const TSNode mn = ts_node_child_by_field_name( stmt, "module_name", 11 ); + const TSNode mn = fieldChild( stmt, NodeField::ModuleName ); if( ts_node_is_null( mn ) ) { return; } target = importSpecifierText( mn, src ); } - const TSNode moduleNode = isFrom ? ts_node_child_by_field_name( stmt, "module_name", 11 ) : TSNode{}; + const TSNode moduleNode = isFrom ? fieldChild( stmt, NodeField::ModuleName ) : TSNode{}; const std::uint32_t n = ts_node_child_count( stmt ); for( std::uint32_t i = 0; i < n; ++i ) { @@ -1779,8 +1781,8 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t std::string clauseTarget; if( kindIs( kt, "aliased_import" ) ) { - const TSNode alias = ts_node_child_by_field_name( kid, "alias", 5 ); - const TSNode nm = ts_node_child_by_field_name( kid, "name", 4 ); + const TSNode alias = fieldChild( kid, NodeField::Alias ); + const TSNode nm = fieldChild( kid, NodeField::Name ); if( ts_node_is_null( alias ) || ts_node_is_null( nm ) ) { continue; @@ -1880,7 +1882,7 @@ void captureIncludes( TSNode root, Lang lang, std::uint32_t fileId, std::string_ std::uint32_t childOpenIdx = frame.openIdx; if( lang == Lang::Ruby && ( kindIs( t, "class" ) || kindIs( t, "module" ) ) ) { - if( const TSNode nm = ts_node_child_by_field_name( n, "name", 4 ); !ts_node_is_null( nm ) ) + if( const TSNode nm = fieldChild( n, NodeField::Name ); !ts_node_is_null( nm ) ) { if( std::string written = rubyConstantText( nm, src ); !written.empty() ) { diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 324ff3104..365461a46 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -111,13 +111,13 @@ void ffiVisitNode( FfiCtx& cx, TSNode n, const char* t ) // pybind11: m.def("alias", &target) / cls.def("alias", &Scope::method) / .def_static(...) if( hasPybind && kindIs( t, "call_expression" ) ) { - const TSNode fn = ts_node_child_by_field_name( n, "function", 8 ); + const TSNode fn = fieldChild( n, NodeField::Function ); if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "field_expression" ) ) { - const std::string_view meth = nodeSrc( ts_node_child_by_field_name( fn, "field", 5 ) ); + const std::string_view meth = nodeSrc( fieldChild( fn, NodeField::Field ) ); if( meth == "def" || meth == "def_static" ) { - const TSNode args = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode args = fieldChild( n, NodeField::Arguments ); std::string alias, tgt; const std::uint32_t cc = ts_node_is_null( args ) ? 0 : ts_node_child_count( args ); for( std::uint32_t i = 0; i < cc; ++i ) @@ -179,7 +179,7 @@ void ffiVisitNode( FfiCtx& cx, TSNode n, const char* t ) inner.pop_back(); if( kindIs( ts_node_type( m ), "function_declarator" ) ) { - const TSNode decl = ts_node_child_by_field_name( m, "declarator", 10 ); + const TSNode decl = fieldChild( m, NodeField::Declarator ); if( !ts_node_is_null( decl ) ) { const char* dt = ts_node_type( decl ); @@ -204,12 +204,12 @@ void ffiVisitNode( FfiCtx& cx, TSNode n, const char* t ) // Python ctypes handle: lib = CDLL(...) / lib = ctypes.CDLL(...) / lib = cdll.LoadLibrary(...) else if( py && kindIs( t, "assignment" ) ) { - const TSNode lhs = ts_node_child_by_field_name( n, "left", 4 ); - const TSNode rhs = ts_node_child_by_field_name( n, "right", 5 ); + const TSNode lhs = fieldChild( n, NodeField::Left ); + const TSNode rhs = fieldChild( n, NodeField::Right ); if( !ts_node_is_null( lhs ) && kindIs( ts_node_type( lhs ), "identifier" ) && !ts_node_is_null( rhs ) && kindIs( ts_node_type( rhs ), "call" ) ) { - const std::string_view ftext = nodeSrc( ts_node_child_by_field_name( rhs, "function", 8 ) ); + const std::string_view ftext = nodeSrc( fieldChild( rhs, NodeField::Function ) ); const std::string seg = finalSegment( ftext.substr( 0, ftext.find( '(' ) ) ); // final `.`/`::` segment const bool loader = seg == "CDLL" || seg == "WinDLL" || seg == "OleDLL" || seg == "PyDLL" || seg == "LoadLibrary" || seg == "dlopen"; @@ -290,7 +290,7 @@ inline std::string lastArgHandlerName( TSNode argsNode, std::string_view src ) } if( kindIs( lt, "member_expression" ) ) { - const TSNode prop = ts_node_child_by_field_name( last, "property", 8 ); + const TSNode prop = fieldChild( last, NodeField::Property ); if( !ts_node_is_null( prop ) ) { return finalSegment( text( prop ) ); @@ -338,7 +338,7 @@ inline HttpMethod pyMethodsKeyword( TSNode argsNode, std::string_view src, bool& { continue; } - const TSNode nameN = ts_node_child_by_field_name( c, "name", 4 ); + const TSNode nameN = fieldChild( c, NodeField::Name ); if( ts_node_is_null( nameN ) ) { continue; @@ -349,7 +349,7 @@ inline HttpMethod pyMethodsKeyword( TSNode argsNode, std::string_view src, bool& continue; } hasKeyword = true; - const TSNode valueN = ts_node_child_by_field_name( c, "value", 5 ); + const TSNode valueN = fieldChild( c, NodeField::Value ); if( ts_node_is_null( valueN ) || !kindIs( ts_node_type( valueN ), "list" ) ) { return HttpMethod::Unknown; @@ -380,7 +380,7 @@ inline HttpMethod jsMethodProperty( TSNode objNode, std::string_view src ) { continue; } - const TSNode keyN = ts_node_child_by_field_name( c, "key", 3 ); + const TSNode keyN = fieldChild( c, NodeField::Key ); if( ts_node_is_null( keyN ) ) { continue; @@ -408,7 +408,7 @@ inline HttpMethod jsMethodProperty( TSNode objNode, std::string_view src ) { continue; } - return stringNodeToMethod( ts_node_child_by_field_name( c, "value", 5 ), src ); + return stringNodeToMethod( fieldChild( c, NodeField::Value ), src ); } return HttpMethod::Unknown; } @@ -461,11 +461,11 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) // Python server: @app.get("/path") / @app.route("/path", methods=[...]) directly above a def. if( pyServerGated && kindIs( t, "decorated_definition" ) ) { - const TSNode defNode = ts_node_child_by_field_name( n, "definition", 10 ); + const TSNode defNode = fieldChild( n, NodeField::Definition ); std::string handlerName; if( !ts_node_is_null( defNode ) && kindIs( ts_node_type( defNode ), "function_definition" ) ) { - const TSNode nameNode = ts_node_child_by_field_name( defNode, "name", 4 ); + const TSNode nameNode = fieldChild( defNode, NodeField::Name ); if( !ts_node_is_null( nameNode ) ) { handlerName.assign( nodeSrc( nameNode ) ); @@ -484,13 +484,13 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) { continue; } - const TSNode fn = ts_node_child_by_field_name( expr, "function", 8 ); + const TSNode fn = fieldChild( expr, NodeField::Function ); if( ts_node_is_null( fn ) || !kindIs( ts_node_type( fn ), "attribute" ) ) { continue; } - const std::string_view attrName = nodeSrc( ts_node_child_by_field_name( fn, "attribute", 9 ) ); - const TSNode argsNode = ts_node_child_by_field_name( expr, "arguments", 9 ); + const std::string_view attrName = nodeSrc( fieldChild( fn, NodeField::Attribute ) ); + const TSNode argsNode = fieldChild( expr, NodeField::Arguments ); const std::string path = firstPathStringArg( argsNode, src ); if( path.empty() ) { @@ -524,12 +524,12 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) // shape see that same node. else if( js && kindIs( t, "call_expression" ) ) { - const TSNode fn = ts_node_child_by_field_name( n, "function", 8 ); + const TSNode fn = fieldChild( n, NodeField::Function ); bool handled = false; if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "identifier" ) && nodeSrc( fn ) == "fetch" ) { - const TSNode argsNode = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode argsNode = fieldChild( n, NodeField::Arguments ); const std::string path = firstPathStringArg( argsNode, src ); if( !path.empty() ) { @@ -544,14 +544,14 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) } else if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "member_expression" ) ) { - const TSNode objN = ts_node_child_by_field_name( fn, "object", 6 ); + const TSNode objN = fieldChild( fn, NodeField::Object ); if( !ts_node_is_null( objN ) && kindIs( ts_node_type( objN ), "identifier" ) && nodeSrc( objN ) == "axios" ) { - const TSNode propN = ts_node_child_by_field_name( fn, "property", 8 ); + const TSNode propN = fieldChild( fn, NodeField::Property ); const HttpMethod method = httpMethodFromName( nodeSrc( propN ) ); if( method != HttpMethod::Unknown ) { - const TSNode argsNode = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode argsNode = fieldChild( n, NodeField::Arguments ); const std::string path = firstPathStringArg( argsNode, src ); if( !path.empty() ) { @@ -567,11 +567,11 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) // only on a file-level framework signal (captureFfi's pybind-gate posture). if( !handled && jsServerGated && !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "member_expression" ) ) { - const TSNode propN = ts_node_child_by_field_name( fn, "property", 8 ); + const TSNode propN = fieldChild( fn, NodeField::Property ); const HttpMethod method = httpMethodFromName( nodeSrc( propN ) ); if( method != HttpMethod::Unknown ) { - const TSNode argsNode = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode argsNode = fieldChild( n, NodeField::Arguments ); const std::string path = firstPathStringArg( argsNode, src ); if( !path.empty() ) { @@ -584,17 +584,17 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) // Python client: requests.get('/path') / requests.post('/path', json=...) else if( py && kindIs( t, "call" ) ) { - const TSNode fn = ts_node_child_by_field_name( n, "function", 8 ); + const TSNode fn = fieldChild( n, NodeField::Function ); if( !ts_node_is_null( fn ) && kindIs( ts_node_type( fn ), "attribute" ) ) { - const TSNode objN = ts_node_child_by_field_name( fn, "object", 6 ); + const TSNode objN = fieldChild( fn, NodeField::Object ); if( !ts_node_is_null( objN ) && kindIs( ts_node_type( objN ), "identifier" ) && nodeSrc( objN ) == "requests" ) { - const TSNode attrN = ts_node_child_by_field_name( fn, "attribute", 9 ); + const TSNode attrN = fieldChild( fn, NodeField::Attribute ); const HttpMethod method = httpMethodFromName( nodeSrc( attrN ) ); if( method != HttpMethod::Unknown ) { - const TSNode argsNode = ts_node_child_by_field_name( n, "arguments", 9 ); + const TSNode argsNode = fieldChild( n, NodeField::Arguments ); const std::string path = firstPathStringArg( argsNode, src ); if( !path.empty() ) { @@ -662,7 +662,7 @@ inline bool isUpdateOrAssignmentTarget( TSNode node ) noexcept || kindIs( pt, "augmented_assignment" ); // Python `+=` … if( isAssign ) { - const TSNode lhs = ts_node_child_by_field_name( parent, "left", 4 ); + const TSNode lhs = fieldChild( parent, NodeField::Left ); return !ts_node_is_null( lhs ) && sameSpan( lhs, node ); } return false; @@ -707,15 +707,15 @@ inline bool isCallCallee( TSNode id ) noexcept // bare call `foo()` — the function field is the identifier itself. if( kindIs( pt, "call_expression" ) || kindIs( pt, "call" ) ) { - const TSNode fn = ts_node_child_by_field_name( parent, "function", 8 ); + const TSNode fn = fieldChild( parent, NodeField::Function ); return !ts_node_is_null( fn ) && sameSpan( fn, id ); } // member call `x.m()` / `x->m()` — `id` is the field of a field_expression/attribute that is the // function of a call. (The receiver `x` is NOT the callee → still captured as a read below.) if( kindIs( pt, "field_expression" ) || kindIs( pt, "attribute" ) ) { - const TSNode fieldNode = ts_node_child_by_field_name( parent, "field", 5 ); - const TSNode attrNode = ts_node_child_by_field_name( parent, "attribute", 9 ); + const TSNode fieldNode = fieldChild( parent, NodeField::Field ); + const TSNode attrNode = fieldChild( parent, NodeField::Attribute ); const bool isField = ( !ts_node_is_null( fieldNode ) && sameSpan( fieldNode, id ) ) || ( !ts_node_is_null( attrNode ) && sameSpan( attrNode, id ) ); if( !isField ) @@ -732,7 +732,7 @@ inline bool isCallCallee( TSNode id ) noexcept { return false; } - const TSNode fn = ts_node_child_by_field_name( gp, "function", 8 ); + const TSNode fn = fieldChild( gp, NodeField::Function ); return !ts_node_is_null( fn ) && sameSpan( fn, parent ); } return false; @@ -752,7 +752,7 @@ inline bool isCallCallee( TSNode id ) noexcept // resolves to an `argument_list`, the same node every genuine call ARGUMENT lives under. Suppressing that // parent would delete real reads corpus-wide to chase a shape that is vanishingly rare in real source. // Iteration 4 adds the shape arm 2 looks straight at and still misses: a `declaration` carries one -// `declarator` FIELD PER DECLARED NAME, so ts_node_child_by_field_name — which returns the FIRST — sees +// `declarator` FIELD PER DECLARED NAME, so a field read — which returns the FIRST — sees // `a` in `int a, key;` and never `key`; a bare `int key;` it misses outright, the parent type not being in // arm 2's list at all. Iterations 1-3 could not observe either, because the block-start span suppressed the // declaration line along with the rest of the block; declaration-point spans stop covering it. @@ -778,7 +778,7 @@ inline bool isDeclSiteName( TSNode id, TSNode parent, const char* pt ) noexcept } if( kindIs( pt, "for_range_loop" ) ) { - const TSNode decl = ts_node_child_by_field_name( parent, "declarator", 10 ); + const TSNode decl = fieldChild( parent, NodeField::Declarator ); return !ts_node_is_null( decl ) && sameSpan( decl, id ); } if( kindIs( pt, "lambda_capture_initializer" ) ) @@ -818,7 +818,7 @@ inline bool isNonValueContext( TSNode id ) noexcept || kindIs( pt, "reference_declarator" ) || kindIs( pt, "array_declarator" ) || kindIs( pt, "optional_parameter_declaration" ) ) { - const TSNode decl = ts_node_child_by_field_name( parent, "declarator", 10 ); + const TSNode decl = fieldChild( parent, NodeField::Declarator ); if( !ts_node_is_null( decl ) && sameSpan( decl, id ) ) { return true; @@ -834,7 +834,7 @@ inline bool isNonValueContext( TSNode id ) noexcept || kindIs( pt, "typed_parameter" ) || kindIs( pt, "default_parameter" ) || kindIs( pt, "lambda_parameters" ) ) { - const TSNode nm = ts_node_child_by_field_name( parent, "name", 4 ); + const TSNode nm = fieldChild( parent, NodeField::Name ); if( ( !ts_node_is_null( nm ) && sameSpan( nm, id ) ) || kindIs( pt, "parameters" ) || kindIs( pt, "lambda_parameters" ) ) { return true; // every direct child of a parameter list is a param NAME, not a use @@ -922,7 +922,7 @@ inline bool isTypeDeclarationSite( TSNode id ) noexcept { if( std::strcmp( pt, k ) == 0 ) { - const TSNode nm = ts_node_child_by_field_name( parent, "name", 4 ); + const TSNode nm = fieldChild( parent, NodeField::Name ); if( !ts_node_is_null( nm ) && sameSpan( nm, id ) ) { return true; @@ -932,7 +932,7 @@ inline bool isTypeDeclarationSite( TSNode id ) noexcept // `typedef struct X Y;` — Y is the DECLARATOR field and is the new name; X keeps its mention. if( kindIs( pt, "type_definition" ) ) { - const TSNode dc = ts_node_child_by_field_name( parent, "declarator", 10 ); + const TSNode dc = fieldChild( parent, NodeField::Declarator ); if( !ts_node_is_null( dc ) && sameSpan( dc, id ) ) { return true; @@ -1470,7 +1470,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // trimming at `(` there would wrongly cut `operator()`, so this is operator_cast-only. if( kindIs( ts_node_type( cap.node ), "operator_cast" ) ) { - const TSNode typeNode = ts_node_child_by_field_name( cap.node, "type", 4 ); + const TSNode typeNode = fieldChild( cap.node, NodeField::Type ); if( !ts_node_is_null( typeNode ) ) { const uint32_t typeEnd = ts_node_end_byte( typeNode ); @@ -1624,7 +1624,7 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t // all reported loc=3439 cx=487). The enclosing statement_block is not in the scope-stop // list above (only C-family compound_statement/block are), so nested defs escaped upward; // containment is the grammar-agnostic stop. Gate: test/jsnestedcheck.sh. - const TSNode pb = ts_node_child_by_field_name( p, "body", 4 ); + const TSNode pb = fieldChild( p, NodeField::Body ); if( !ts_node_is_null( pb ) ) { if( !spanContains( pb, roleNode ) ) { defNode = p; body = pb; } @@ -1797,10 +1797,10 @@ void captureTagsFacts( TSQueryCursor* cursor, const LangEntry& le, std::uint32_t if( le.lang == Lang::Elixir ) { r.qualifier = elixirScope( roleNode, src ); - const TSNode target = ts_node_child_by_field_name( roleNode, "target", 6 ); + const TSNode target = fieldChild( roleNode, NodeField::Target ); if( !ts_node_is_null( target ) && kindIs( ts_node_type( target ), "dot" ) ) { - const TSNode receiver = ts_node_child_by_field_name( target, "left", 4 ); + const TSNode receiver = fieldChild( target, NodeField::Left ); if( !ts_node_is_null( receiver ) && kindIs( ts_node_type( receiver ), "alias" ) ) { r.qualifier = std::string( nodeTextOf( receiver, src ) ); diff --git a/src/preprocdead.h b/src/preprocdead.h index df5f96239..4d144379f 100644 --- a/src/preprocdead.h +++ b/src/preprocdead.h @@ -36,6 +36,8 @@ #include +#include "infra/fieldid.h" // rw::fieldChild / NodeField — the field id resolved once per grammar, not per node + namespace rw { @@ -71,7 +73,7 @@ inline PreprocLiteral preprocLiteralBranch( TSNode n, std::string_view src ) noe { return PreprocLiteral::Undecided; } - const TSNode cond = ts_node_child_by_field_name( n, "condition", 9 ); + const TSNode cond = fieldChild( n, NodeField::Condition ); if( ts_node_is_null( cond ) || !preprocNodeKindIs( cond, "number_literal" ) ) { return PreprocLiteral::Undecided; @@ -117,8 +119,8 @@ inline std::vector collectPreprocDeadRanges( TSNode root, std: const PreprocLiteral lit = preprocLiteralBranch( n, src ); if( lit != PreprocLiteral::Undecided ) { - const TSNode cond = ts_node_child_by_field_name( n, "condition", 9 ); - const TSNode alt = ts_node_child_by_field_name( n, "alternative", 11 ); + const TSNode cond = fieldChild( n, NodeField::Condition ); + const TSNode alt = fieldChild( n, NodeField::Alternative ); const std::uint32_t nEnd = ts_node_end_byte( n ); if( lit == PreprocLiteral::BodyDead ) { diff --git a/src/slice.h b/src/slice.h index eebe1812e..dc0a70fd4 100644 --- a/src/slice.h +++ b/src/slice.h @@ -57,6 +57,7 @@ #include "sarif.h" // rootPrefixOf / rootRelativeUri — root-relative p=, same as every verb #include "infra/Diagnostics.h" // DEGRADED_PATH_ALERT — the three parse-refusal arms are degrades, not asserts +#include "infra/fieldid.h" // rw::fieldChild / NodeField — the field id resolved once per grammar, not per node #include @@ -396,13 +397,13 @@ inline std::vector sliceSeedLineLocals( const SliceScan& scan, std: return out; } -inline TSNode sliceField( TSNode p, const char* field ) noexcept +inline TSNode sliceField( TSNode p, NodeField field ) noexcept { - return ts_node_child_by_field_name( p, field, std::uint32_t( std::strlen( field ) ) ); + return fieldChild( p, field ); } // n IS the field child (identity, not containment) — the precise arm: `x = …` defs x, `arr[i] = …` does not def i -inline bool sliceIsField( TSNode p, const char* field, TSNode n ) noexcept +inline bool sliceIsField( TSNode p, NodeField field, TSNode n ) noexcept { const TSNode c = sliceField( p, field ); if( ts_node_is_null( c ) ) @@ -415,7 +416,7 @@ inline bool sliceIsField( TSNode p, const char* field, TSNode n ) noexcept // n lies WITHIN the field child's byte span — the containment arm, for pattern-shaped fields (Rust // `mut x`, Python tuples). ingest.cpp's spanContains is .cpp-private, so the range compare lives inline // here rather than growing an export for two comparisons. -inline bool sliceInField( TSNode p, const char* field, TSNode n ) noexcept +inline bool sliceInField( TSNode p, NodeField field, TSNode n ) noexcept { const TSNode outer = sliceField( p, field ); if( ts_node_is_null( outer ) ) @@ -435,7 +436,7 @@ inline bool sliceIsJsPatternKind( TSNode n ) noexcept // the assignment operator's own text — "+=", "=", … — read to split a plain write from a read-modify-write inline bool sliceOperatorIsPlainAssign( TSNode assignNode, std::string_view src ) noexcept { - const TSNode op = sliceField( assignNode, "operator" ); + const TSNode op = sliceField( assignNode, NodeField::Operator ); if( ts_node_is_null( op ) ) { return true; // no operator field captured — treat as plain (a def, not a def+use guess) @@ -453,7 +454,7 @@ inline bool sliceOperatorIsPlainAssign( TSNode assignNode, std::string_view src inline bool sliceIsDirectInitCtorArg( TSNode n ) noexcept { const TSNode p = ts_node_parent( n ); - if( ts_node_is_null( p ) || !sliceKindIs( p, "parameter_declaration" ) || !sliceIsField( p, "type", n ) ) + if( ts_node_is_null( p ) || !sliceKindIs( p, "parameter_declaration" ) || !sliceIsField( p, NodeField::Type, n ) ) { return false; } @@ -483,8 +484,8 @@ inline bool sliceClassifyJsBinder( TSNode n, TSNode p, const char* pk, SliceOcc& const char* dk = pk; while( !ts_node_is_null( pp ) && sliceIsJsPatternKind( pp ) ) { - if( ( std::strcmp( dk, "pair_pattern" ) == 0 && !sliceInField( pp, "value", d ) ) - || ( ( std::strcmp( dk, "object_assignment_pattern" ) == 0 || std::strcmp( dk, "assignment_pattern" ) == 0 ) && !sliceInField( pp, "left", d ) ) ) + if( ( std::strcmp( dk, "pair_pattern" ) == 0 && !sliceInField( pp, NodeField::Value, d ) ) + || ( ( std::strcmp( dk, "object_assignment_pattern" ) == 0 || std::strcmp( dk, "assignment_pattern" ) == 0 ) && !sliceInField( pp, NodeField::Left, d ) ) ) { return false; // the key / default side: a read } @@ -499,9 +500,9 @@ inline bool sliceClassifyJsBinder( TSNode n, TSNode p, const char* pk, SliceOcc& // identity when n sits directly in the field (`arr[i] = …` must not def i), containment once a // pattern was climbed (the field then holds the pattern, not the identifier) const bool climbed = !ts_node_eq( d, n ); - const auto inField = [ & ]( const char* field ) noexcept { return climbed ? sliceInField( pp, field, d ) : sliceIsField( pp, field, n ); }; + const auto inField = [ & ]( NodeField field ) noexcept { return climbed ? sliceInField( pp, field, d ) : sliceIsField( pp, field, n ); }; const auto def = [ & ]( OccT t ) noexcept { o.t = t; o.isDef = true; return true; }; - if( std::strcmp( dk, "variable_declarator" ) == 0 && inField( "name" ) ) + if( std::strcmp( dk, "variable_declarator" ) == 0 && inField( NodeField::Name ) ) { return def( OccT::Decl ); // let count = 0; const { x } = o; } @@ -509,19 +510,19 @@ inline bool sliceClassifyJsBinder( TSNode n, TSNode p, const char* pk, SliceOcc& { return def( OccT::Param ); // function f(count) f({ p }, [ q ]) f(count = 0) } - if( ( std::strcmp( dk, "required_parameter" ) == 0 || std::strcmp( dk, "optional_parameter" ) == 0 ) && inField( "pattern" ) ) + if( ( std::strcmp( dk, "required_parameter" ) == 0 || std::strcmp( dk, "optional_parameter" ) == 0 ) && inField( NodeField::Pattern ) ) { return def( OccT::Param ); // TS: (count: number) ({ p }: T) } - if( std::strcmp( dk, "assignment_expression" ) == 0 && inField( "left" ) ) + if( std::strcmp( dk, "assignment_expression" ) == 0 && inField( NodeField::Left ) ) { return def( OccT::Assign ); // count = … ({ x } = o) } - if( std::strcmp( dk, "for_in_statement" ) == 0 && inField( "left" ) ) + if( std::strcmp( dk, "for_in_statement" ) == 0 && inField( NodeField::Left ) ) { return def( OccT::Decl ); // for (x of xs) for (const { k } of xs) } - if( std::strcmp( dk, "catch_clause" ) == 0 && inField( "parameter" ) ) + if( std::strcmp( dk, "catch_clause" ) == 0 && inField( NodeField::Parameter ) ) { return def( OccT::Decl ); // catch (e) catch ({ message }) } @@ -569,27 +570,27 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no } if( !ts_node_is_null( pp ) ) { - if( std::strcmp( dk, "init_declarator" ) == 0 && sliceInField( pp, "declarator", d ) ) + if( std::strcmp( dk, "init_declarator" ) == 0 && sliceInField( pp, NodeField::Declarator, d ) ) { def( OccT::Decl ); return o; // int count = 0; (the value side falls through to uses) } - if( std::strcmp( dk, "declaration" ) == 0 && !sliceInField( pp, "type", d ) && !sliceInField( pp, "value", d ) ) + if( std::strcmp( dk, "declaration" ) == 0 && !sliceInField( pp, NodeField::Type, d ) && !sliceInField( pp, NodeField::Value, d ) ) { def( OccT::Decl ); return o; // int count; — but not the `x` of `if( int k = x )`: tree-sitter-cpp's } // condition-clause declaration carries its initializer in a `value` field // with no init_declarator, and that x is a READ (a false def here became a // false binding once block scopes were separated, 2026-09-02) if( ( std::strcmp( dk, "parameter_declaration" ) == 0 || std::strcmp( dk, "optional_parameter_declaration" ) == 0 ) - && !sliceInField( pp, "type", d ) && !sliceInField( pp, "default_value", d ) ) + && !sliceInField( pp, NodeField::Type, d ) && !sliceInField( pp, NodeField::DefaultValue, d ) ) { def( OccT::Param ); return o; // int limit — a default value's identifiers stay uses } - if( std::strcmp( dk, "for_range_loop" ) == 0 && sliceInField( pp, "declarator", d ) ) + if( std::strcmp( dk, "for_range_loop" ) == 0 && sliceInField( pp, NodeField::Declarator, d ) ) { def( OccT::Decl ); return o; // for( auto x : v ) } } - if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, "left", n ) ) + if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, NodeField::Left, n ) ) { if( sliceOperatorIsPlainAssign( p, src ) ) { def( OccT::Assign ); } else { both( OccT::Assign ); } return o; @@ -615,19 +616,19 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no { def( OccT::Param ); return o; // def f(n): } - if( std::strcmp( pk, "typed_parameter" ) == 0 && !sliceInField( p, "type", n ) ) + if( std::strcmp( pk, "typed_parameter" ) == 0 && !sliceInField( p, NodeField::Type, n ) ) { def( OccT::Param ); return o; // def f(n: int): } if( ( std::strcmp( pk, "default_parameter" ) == 0 || std::strcmp( pk, "typed_default_parameter" ) == 0 ) - && sliceIsField( p, "name", n ) ) + && sliceIsField( p, NodeField::Name, n ) ) { def( OccT::Param ); return o; // def f(n=0): — the default's identifiers stay uses } if( std::strcmp( pk, "assignment" ) == 0 || std::strcmp( pk, "augmented_assignment" ) == 0 ) { const bool aug = std::strcmp( pk, "augmented_assignment" ) == 0; - if( sliceIsField( p, "left", n ) ) + if( sliceIsField( p, NodeField::Left, n ) ) { if( aug ) { both( OccT::Assign ); } else { def( OccT::Assign ); } return o; @@ -638,26 +639,26 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no // a, b = … / for a, b in …: the list itself sits in the enclosing left/target field const TSNode gp = ts_node_parent( p ); if( !ts_node_is_null( gp ) - && ( ( sliceKindIs( gp, "assignment" ) && sliceInField( gp, "left", n ) ) - || ( sliceKindIs( gp, "for_statement" ) && sliceInField( gp, "left", n ) ) - || ( sliceKindIs( gp, "for_in_clause" ) && sliceInField( gp, "left", n ) ) ) ) + && ( ( sliceKindIs( gp, "assignment" ) && sliceInField( gp, NodeField::Left, n ) ) + || ( sliceKindIs( gp, "for_statement" ) && sliceInField( gp, NodeField::Left, n ) ) + || ( sliceKindIs( gp, "for_in_clause" ) && sliceInField( gp, NodeField::Left, n ) ) ) ) { def( OccT::Decl ); return o; } } - if( ( std::strcmp( pk, "for_statement" ) == 0 || std::strcmp( pk, "for_in_clause" ) == 0 ) && sliceInField( p, "left", n ) ) + if( ( std::strcmp( pk, "for_statement" ) == 0 || std::strcmp( pk, "for_in_clause" ) == 0 ) && sliceInField( p, NodeField::Left, n ) ) { def( OccT::Decl ); return o; // for total in …: } - if( std::strcmp( pk, "named_expression" ) == 0 && sliceIsField( p, "name", n ) ) + if( std::strcmp( pk, "named_expression" ) == 0 && sliceIsField( p, NodeField::Name, n ) ) { def( OccT::Assign ); return o; // (total := …) } - if( std::strcmp( pk, "as_pattern_target" ) == 0 || ( std::strcmp( pk, "as_pattern" ) == 0 && sliceInField( p, "alias", n ) ) ) + if( std::strcmp( pk, "as_pattern_target" ) == 0 || ( std::strcmp( pk, "as_pattern" ) == 0 && sliceInField( p, NodeField::Alias, n ) ) ) { def( OccT::Decl ); return o; // with open(…) as f: } - if( std::strcmp( pk, "keyword_argument" ) == 0 && sliceIsField( p, "name", n ) ) + if( std::strcmp( pk, "keyword_argument" ) == 0 && sliceIsField( p, NodeField::Name, n ) ) { o.skip = true; return o; // f(count=3) — the NAME is the callee's keyword, not this local } @@ -682,7 +683,7 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no { return o; // a declarator / parameter / for-of / assignment binder, destructured or plain } - if( std::strcmp( pk, "augmented_assignment_expression" ) == 0 && sliceIsField( p, "left", n ) ) + if( std::strcmp( pk, "augmented_assignment_expression" ) == 0 && sliceIsField( p, NodeField::Left, n ) ) { both( OccT::Assign ); return o; // count += n } @@ -712,24 +713,24 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no } } const char* ek = ts_node_type( eff ); - if( std::strcmp( ek, "short_var_declaration" ) == 0 && sliceIsField( eff, "left", effChild ) ) + if( std::strcmp( ek, "short_var_declaration" ) == 0 && sliceIsField( eff, NodeField::Left, effChild ) ) { def( OccT::Decl ); return o; // count := 0 } - if( std::strcmp( ek, "assignment_statement" ) == 0 && sliceIsField( eff, "left", effChild ) ) + if( std::strcmp( ek, "assignment_statement" ) == 0 && sliceIsField( eff, NodeField::Left, effChild ) ) { if( sliceOperatorIsPlainAssign( eff, src ) ) { def( OccT::Assign ); } else { both( OccT::Assign ); } return o; } - if( std::strcmp( ek, "range_clause" ) == 0 && sliceIsField( eff, "left", effChild ) ) + if( std::strcmp( ek, "range_clause" ) == 0 && sliceIsField( eff, NodeField::Left, effChild ) ) { def( OccT::Decl ); return o; // for i, v := range xs } - if( std::strcmp( pk, "var_spec" ) == 0 && !sliceInField( p, "type", n ) && !sliceInField( p, "value", n ) ) + if( std::strcmp( pk, "var_spec" ) == 0 && !sliceInField( p, NodeField::Type, n ) && !sliceInField( p, NodeField::Value, n ) ) { def( OccT::Decl ); return o; // var count int } - if( std::strcmp( pk, "parameter_declaration" ) == 0 && !sliceInField( p, "type", n ) ) + if( std::strcmp( pk, "parameter_declaration" ) == 0 && !sliceInField( p, NodeField::Type, n ) ) { def( OccT::Param ); return o; // func f(count int) } @@ -746,19 +747,19 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no case SliceFam::Java: { - if( std::strcmp( pk, "variable_declarator" ) == 0 && sliceIsField( p, "name", n ) ) + if( std::strcmp( pk, "variable_declarator" ) == 0 && sliceIsField( p, NodeField::Name, n ) ) { def( OccT::Decl ); return o; // int count = 0; } - if( std::strcmp( pk, "formal_parameter" ) == 0 && sliceIsField( p, "name", n ) ) + if( std::strcmp( pk, "formal_parameter" ) == 0 && sliceIsField( p, NodeField::Name, n ) ) { def( OccT::Param ); return o; } - if( std::strcmp( pk, "enhanced_for_statement" ) == 0 && sliceIsField( p, "name", n ) ) + if( std::strcmp( pk, "enhanced_for_statement" ) == 0 && sliceIsField( p, NodeField::Name, n ) ) { def( OccT::Decl ); return o; // for (int x : xs) } - if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, "left", n ) ) + if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, NodeField::Left, n ) ) { if( sliceOperatorIsPlainAssign( p, src ) ) { def( OccT::Assign ); } else { both( OccT::Assign ); } return o; @@ -782,7 +783,7 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no if( std::strcmp( pk, "let_declaration" ) == 0 || ( !ts_node_is_null( gp ) && sliceKindIs( gp, "let_declaration" ) ) ) { const TSNode letNode = std::strcmp( pk, "let_declaration" ) == 0 ? p : gp; - if( sliceInField( letNode, "pattern", n ) ) + if( sliceInField( letNode, NodeField::Pattern, n ) ) { def( OccT::Decl ); return o; } @@ -790,7 +791,7 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no if( std::strcmp( pk, "parameter" ) == 0 || ( !ts_node_is_null( gp ) && sliceKindIs( gp, "parameter" ) ) ) { const TSNode parNode = std::strcmp( pk, "parameter" ) == 0 ? p : gp; - if( sliceInField( parNode, "pattern", n ) ) + if( sliceInField( parNode, NodeField::Pattern, n ) ) { def( OccT::Param ); return o; } @@ -799,15 +800,15 @@ inline SliceOcc sliceClassify( TSNode n, SliceFam fam, std::string_view src ) no { def( OccT::Param ); return o; // |count| … } - if( std::strcmp( pk, "for_expression" ) == 0 && sliceInField( p, "pattern", n ) ) + if( std::strcmp( pk, "for_expression" ) == 0 && sliceInField( p, NodeField::Pattern, n ) ) { def( OccT::Decl ); return o; // for x in xs } - if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, "left", n ) ) + if( std::strcmp( pk, "assignment_expression" ) == 0 && sliceIsField( p, NodeField::Left, n ) ) { def( OccT::Assign ); return o; } - if( std::strcmp( pk, "compound_assignment_expr" ) == 0 && sliceIsField( p, "left", n ) ) + if( std::strcmp( pk, "compound_assignment_expr" ) == 0 && sliceIsField( p, NodeField::Left, n ) ) { both( OccT::Assign ); return o; // count += n } @@ -1081,9 +1082,9 @@ template< class WalkFn > inline void sliceWalkPreproc( TSNode node, const SliceWalkCtx& ctx, SliceScan& scan, SlicePp pp, const WalkFn& walk ) { const auto [ bodyState, altState ] = slicePreprocBranchStates( node, ctx.src, pp ); - const TSNode condition = sliceField( node, "condition" ); - const TSNode macroName = sliceField( node, "name" ); - const TSNode alternative = sliceField( node, "alternative" ); + const TSNode condition = sliceField( node, NodeField::Condition ); + const TSNode macroName = sliceField( node, NodeField::Name ); + const TSNode alternative = sliceField( node, NodeField::Alternative ); const std::uint32_t ppChildCount = ts_node_child_count( node ); for( std::uint32_t childIndex = 0; childIndex < ppChildCount; ++childIndex ) { @@ -1530,39 +1531,39 @@ struct SliceRdWalker { if( sliceKindIs( n, "if_statement" ) ) { - condition( sliceField( n, "condition" ), state ); + condition( sliceField( n, NodeField::Condition ), state ); SliceRdState thenS = state, elseS = state; - stmt( sliceField( n, "consequence" ), thenS ); - branchBody( sliceField( n, "alternative" ), elseS ); + stmt( sliceField( n, NodeField::Consequence ), thenS ); + branchBody( sliceField( n, NodeField::Alternative ), elseS ); sliceRdJoin( thenS, elseS ); state = thenS; return true; } if( sliceKindIs( n, "while_statement" ) ) { - const TSNode cond = sliceField( n, "condition" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { condition( cond, s ); exit = s; }, sliceField( n, "body" ), TSNode{}, TSNode{}, state, false ); + const TSNode cond = sliceField( n, NodeField::Condition ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { condition( cond, s ); exit = s; }, sliceField( n, NodeField::Body ), TSNode{}, TSNode{}, state, false ); return true; } if( sliceKindIs( n, "do_statement" ) ) { - const TSNode cond = sliceField( n, "condition" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, "body" ), TSNode{}, TSNode{}, state, true ); + const TSNode cond = sliceField( n, NodeField::Condition ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, NodeField::Body ), TSNode{}, TSNode{}, state, true ); return true; } if( sliceKindIs( n, "for_statement" ) ) { - unit( sliceField( n, "initializer" ), state ); - const TSNode cond = sliceField( n, "condition" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, "body" ), sliceField( n, "update" ), TSNode{}, state, false ); + unit( sliceField( n, NodeField::Initializer ), state ); + const TSNode cond = sliceField( n, NodeField::Condition ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, NodeField::Body ), sliceField( n, NodeField::Update ), TSNode{}, state, false ); return true; } if( sliceKindIs( n, "for_range_loop" ) ) { - unit( sliceField( n, "initializer" ), state ); // C++20 `for( init; x : r )` - unit( sliceField( n, "right" ), state ); // the range, evaluated once - const TSNode decl = sliceField( n, "declarator" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { exit = s; unit( decl, s ); }, sliceField( n, "body" ), TSNode{}, TSNode{}, state, false ); + unit( sliceField( n, NodeField::Initializer ), state ); // C++20 `for( init; x : r )` + unit( sliceField( n, NodeField::Right ), state ); // the range, evaluated once + const TSNode decl = sliceField( n, NodeField::Declarator ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { exit = s; unit( decl, s ); }, sliceField( n, NodeField::Body ), TSNode{}, TSNode{}, state, false ); return true; } if( sliceKindIs( n, "switch_statement" ) ) @@ -1612,12 +1613,12 @@ struct SliceRdWalker // it; break leaves; no default keeps the "no case matched" path void switchC( TSNode n, SliceRdState& state ) { - unit( sliceField( n, "condition" ), state ); + unit( sliceField( n, NodeField::Condition ), state ); const SliceRdState in = state; SliceRdState brk = dead(), fall = dead(); bool hasDefault = false; breakAcc.push_back( &brk ); - const TSNode body = sliceField( n, "body" ); + const TSNode body = sliceField( n, NodeField::Body ); const std::uint32_t childCount = ts_node_is_null( body ) ? 0 : ts_node_named_child_count( body ); for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) { @@ -1629,7 +1630,7 @@ struct SliceRdWalker } SliceRdState s = in; sliceRdJoin( s, fall ); - const TSNode value = sliceField( c, "value" ); + const TSNode value = sliceField( c, NodeField::Value ); if( ts_node_is_null( value ) ) { hasDefault = true; @@ -1666,7 +1667,7 @@ struct SliceRdWalker SliceRdState handlerIn = dead(); tryAcc.push_back( &handlerIn ); SliceRdState tryOut = state; - stmt( sliceField( n, "body" ), tryOut ); + stmt( sliceField( n, NodeField::Body ), tryOut ); tryAcc.pop_back(); SliceRdState out = tryOut; const std::uint32_t childCount = ts_node_named_child_count( n ); @@ -1678,8 +1679,8 @@ struct SliceRdWalker continue; } SliceRdState h = handlerIn; - unit( sliceField( c, "parameters" ), h ); - stmt( sliceField( c, "body" ), h ); + unit( sliceField( c, NodeField::Parameters ), h ); + stmt( sliceField( c, NodeField::Body ), h ); sliceRdJoin( out, h ); } state = out; @@ -1691,9 +1692,9 @@ struct SliceRdWalker void preprocC( TSNode n, SliceRdState& state ) { const auto [ bodyState, altState ] = slicePreprocBranchStates( n, src, SlicePp::Live ); - const TSNode condition = sliceField( n, "condition" ); - const TSNode macroName = sliceField( n, "name" ); - const TSNode alternative = sliceField( n, "alternative" ); + const TSNode condition = sliceField( n, NodeField::Condition ); + const TSNode macroName = sliceField( n, NodeField::Name ); + const TSNode alternative = sliceField( n, NodeField::Alternative ); SliceRdState bodyOut = bodyState == SlicePp::Dead ? dead() : state; const std::uint32_t childCount = ts_node_named_child_count( n ); for( std::uint32_t childIndex = 0; childIndex < childCount && !bodyOut.dead; ++childIndex ) @@ -1733,19 +1734,19 @@ struct SliceRdWalker } if( sliceKindIs( n, "while_statement" ) ) { - const TSNode cond = sliceField( n, "condition" ); - const TSNode alt = sliceField( n, "alternative" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, "body" ), TSNode{}, - ts_node_is_null( alt ) ? TSNode{} : sliceField( alt, "body" ), state, false ); + const TSNode cond = sliceField( n, NodeField::Condition ); + const TSNode alt = sliceField( n, NodeField::Alternative ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { unit( cond, s ); exit = s; }, sliceField( n, NodeField::Body ), TSNode{}, + ts_node_is_null( alt ) ? TSNode{} : sliceField( alt, NodeField::Body ), state, false ); return true; } if( sliceKindIs( n, "for_statement" ) ) { - unit( sliceField( n, "right" ), state ); // the iterable, evaluated once - const TSNode left = sliceField( n, "left" ); - const TSNode alt = sliceField( n, "alternative" ); - loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { exit = s; unit( left, s ); }, sliceField( n, "body" ), TSNode{}, - ts_node_is_null( alt ) ? TSNode{} : sliceField( alt, "body" ), state, false ); + unit( sliceField( n, NodeField::Right ), state ); // the iterable, evaluated once + const TSNode left = sliceField( n, NodeField::Left ); + const TSNode alt = sliceField( n, NodeField::Alternative ); + loop( [ & ]( SliceRdState& s, SliceRdState& exit ) { exit = s; unit( left, s ); }, sliceField( n, NodeField::Body ), TSNode{}, + ts_node_is_null( alt ) ? TSNode{} : sliceField( alt, NodeField::Body ), state, false ); return true; } if( sliceKindIs( n, "try_statement" ) ) @@ -1755,7 +1756,7 @@ struct SliceRdWalker } if( sliceKindIs( n, "with_statement" ) ) { - const TSNode body = sliceField( n, "body" ); + const TSNode body = sliceField( n, NodeField::Body ); const std::uint32_t childCount = ts_node_named_child_count( n ); for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) { @@ -1797,11 +1798,11 @@ struct SliceRdWalker // if / elif / else: each arm enters from the previous condition's false path; no else keeps that path void ifPy( TSNode n, SliceRdState& state ) { - unit( sliceField( n, "condition" ), state ); + unit( sliceField( n, NodeField::Condition ), state ); SliceRdState falseS = state, out = dead(); { SliceRdState t = state; - stmt( sliceField( n, "consequence" ), t ); + stmt( sliceField( n, NodeField::Consequence ), t ); sliceRdJoin( out, t ); } bool hasElse = false; @@ -1811,15 +1812,15 @@ struct SliceRdWalker const TSNode c = ts_node_named_child( n, childIndex ); if( sliceKindIs( c, "elif_clause" ) ) { - unit( sliceField( c, "condition" ), falseS ); + unit( sliceField( c, NodeField::Condition ), falseS ); SliceRdState t = falseS; - stmt( sliceField( c, "consequence" ), t ); + stmt( sliceField( c, NodeField::Consequence ), t ); sliceRdJoin( out, t ); } else if( sliceKindIs( c, "else_clause" ) ) { SliceRdState t = falseS; - stmt( sliceField( c, "body" ), t ); + stmt( sliceField( c, NodeField::Body ), t ); sliceRdJoin( out, t ); hasElse = true; } @@ -1839,7 +1840,7 @@ struct SliceRdWalker SliceRdState handlerIn = dead(); tryAcc.push_back( &handlerIn ); SliceRdState tryOut = state; - stmt( sliceField( n, "body" ), tryOut ); + stmt( sliceField( n, NodeField::Body ), tryOut ); tryAcc.pop_back(); SliceRdState handlersOut = dead(), normalOut = tryOut; TSNode finallyClause{}; @@ -1867,7 +1868,7 @@ struct SliceRdWalker } else if( sliceKindIs( c, "else_clause" ) ) { - stmt( sliceField( c, "body" ), normalOut ); + stmt( sliceField( c, NodeField::Body ), normalOut ); } else if( sliceKindIs( c, "finally_clause" ) ) { @@ -1892,8 +1893,8 @@ struct SliceRdWalker // is never proven here) void matchPy( TSNode n, SliceRdState& state ) { - unit( sliceField( n, "subject" ), state ); - const TSNode body = sliceField( n, "body" ); + unit( sliceField( n, NodeField::Subject ), state ); + const TSNode body = sliceField( n, NodeField::Body ); SliceRdState out = state; const std::uint32_t childCount = ts_node_is_null( body ) ? 0 : ts_node_named_child_count( body ); for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) @@ -1904,7 +1905,7 @@ struct SliceRdWalker continue; } SliceRdState s = state; - const TSNode consequence = sliceField( c, "consequence" ); + const TSNode consequence = sliceField( c, NodeField::Consequence ); const std::uint32_t partCount = ts_node_named_child_count( c ); for( std::uint32_t partIndex = 0; partIndex < partCount; ++partIndex ) { diff --git a/test/fieldidcheck.sh b/test/fieldidcheck.sh new file mode 100755 index 000000000..cced1db33 --- /dev/null +++ b/test/fieldidcheck.sh @@ -0,0 +1,509 @@ +#!/usr/bin/env bash +# fieldidcheck.sh — gate for rw::fieldChild / the [grammar][field] TSFieldId table (src/infra/fieldid.h), +# which replaced 199 per-AST-node `ts_node_child_by_field_name` calls (each one a linear `strncmp` scan +# over the grammar's field table, through two dyld stubs) with one resolution per grammar at prewarm. +# +# The claim being gated is narrow and total: for EVERY grammar the crawl table can name, EVERY field the +# tree asks for, and EVERY node of a real parse tree, `fieldChild( n, NodeField::X )` returns the SAME +# node `ts_node_child_by_field_name( n, "x", len )` would have. A hoisted lookup table is the kind of +# change that is right on the grammar you tested and wrong on the one you did not, so this gate does not +# sample — it enumerates, over grammars harvested FROM THE TREE (src/ingest_crawl.h's kLangTable) rather +# than from a list frozen in this file. +# +# A. TABLE IDENTITY. For every ( grammar, NodeField ) pair, the warm table's id equals what +# `ts_language_field_id_for_name` answers at runtime for that field's spelling. The spellings the +# reference side uses are harvested from a PRISTINE src/infra/fieldid.h at gate time and baked into +# the harness as literals, so a mutation of the header's own name table cannot move both sides at +# once (arm D). +# A0. COLD PARITY. The same equality BEFORE any grammar is warmed — the unwarmed path must be the +# by-name answer, not a hole. This is what makes "a missed warm is slower, never wrong" a tested +# statement instead of a comment. +# B. ENUMERATED PAIRS, over real trees. For a fixture file per extension row of kLangTable, the whole +# AST is walked and every ( node, field ) pair is compared node-for-node against the by-name call: +# both null, or `ts_node_eq`. This is the arm that can see a wrong id, a wrong grammar keyed, or a +# registry that published a slot's ids under another slot's pointer. +# C. UNKNOWN-FIELD PARITY, and it is not vacuous. A grammar that has no `receiver:` resolves the name +# to id 0, and `ts_node_child_by_field_id( n, 0 )` returns the null node on its first line +# (node.c:602) — so a 0 in the table IS the by-name answer, not a bug to guard. Arm C counts the +# ( grammar, field ) pairs where the id is 0, requires that count to be non-zero (otherwise the arm +# proves nothing), and requires the by-name call to agree on every node of every fixture. +# D. MUTATION. Arms A and B must be able to go red, so two scratch copies of the header are built and +# each must fail: D1 misspells ONE field's row in kNodeFieldNames (the table's data), D2 shifts the +# field index inside the lookup (the table's code). An arm that has never been observed failing is +# decoration. +# E. POPULATION. The conversion must still BE the conversion: zero `ts_node_child_by_field_name` sites +# left in src/ outside fieldid.h's own prose, a non-trivial number of `fieldChild(` sites, and the +# per-grammar warm actually wired into the ingest translation unit. (E-mut) performs the reverse +# rewrite on a scratch copy and requires arm E to report every converted file — a mechanical 199-site +# conversion is exactly where an arm can be green in BOTH directions. +# HONEST LIMIT, stated rather than implied: arm E reads SOURCE. It proves the warm call is written +# and reachable from ingest(); it does not prove at runtime that no grammar took the by-name +# fallback. The runtime evidence for that is the A/B CPU measurement in the landing commit, not a +# gate — instrumenting it would mean a second full build inside a gate, which this repo has a +# recorded trap for. +# F. CAPACITY. kLangTable's distinct grammar count must fit kFieldIdCapacity — a grammar that does not +# fit is correct but silently un-warmed, so the arm is the alarm for the day a 65th grammar lands. +# +# Usage: bash test/fieldidcheck.sh [ CXX=clang++ ] [ RIPWIRE_BIN=build/ripwire ] +# RIPWIRE_BIN is used ONLY to locate the build directory holding the compiled grammar objects (the vendored +# grammars and the tree-sitter core the harness links against) — this gate never EXECUTES the ripwire +# binary. It still needs no pin in test/binoverridecheck.sh's EXEMPT dict: a sentinel RIPWIRE_BIN points at +# a directory with no grammar objects in it, so the gate goes red rather than silently green. +# Exits non-zero on any failure. Does NOT edit regression.sh. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +CXX="${CXX:-c++}" +. "$ROOT/scripts/cxxstd.sh" +CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +HDR="$ROOT/src/infra/fieldid.h" +CRAWL="$ROOT/src/ingest_crawl.h" +# The files the conversion touched. Sections of src/ingest.cpp's TU, plus the two standalone consumers. +SITE_FILES="src/ingest_metrics.h src/ingest_binds.h src/ingest_sidecap.h src/ingest_relations.h src/ingest_names.h src/ingest_jsimports.h src/ingest_elixir.h src/preprocdead.h src/slice.h" + +[ -f "$HDR" ] || { echo "missing $HDR — nothing to check"; exit 2; } +[ -f "$CRAWL" ] || { echo "missing $CRAWL — the grammar table is what this gate enumerates over"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } + +BIN="${RIPWIRE_BIN:-$ROOT/build/ripwire}" +case "$BIN" in /*) ;; *) BIN="$ROOT/$BIN";; esac +BUILDDIR="$( dirname "$BIN" )" +TSLIB="$BUILDDIR/_deps/tree_sitter-build/libtree-sitter.a" +if [ ! -f "$TSLIB" ]; then + echo " no tree-sitter static lib under $BUILDDIR — build first (cmake --build build -j)"; exit 2 +fi +GRAMMAR_OBJS="$( find "$BUILDDIR/CMakeFiles" -type d -name 'ts_*.dir' -exec find {} -name '*.o' \; 2>/dev/null | sort )" +if [ -z "$GRAMMAR_OBJS" ]; then + echo " no compiled grammar objects under $BUILDDIR/CMakeFiles — build first (cmake --build build -j)"; exit 2 +fi +echo "fieldidcheck: CXX=$CXX header=src/infra/fieldid.h build=$BUILDDIR" + +# ── harvest the field spellings FROM A PRISTINE HEADER ─────────────────────────────────────────────── +# Enumerator order and the { "spelling", len } rows, read as TEXT. The harness's reference side uses +# these literals, which is what lets arm D mutate the header under test without moving the reference. +NAMES="$TMP/names.txt" +python3 - "$HDR" > "$NAMES" <<'PYNAMES' +import re, sys +src = open( sys.argv[1] ).read() +enum = re.search( r'enum class NodeField\s*:\s*std::uint8_t\s*\{(.*?)\}', src, re.S ) +if enum is None: sys.exit( "could not find enum class NodeField" ) +members = [ m.strip() for m in enum.group( 1 ).replace( '\n', ' ' ).split( ',' ) if m.strip() ] +if members[ -1 ] != 'Count': sys.exit( "NodeField's last enumerator must be Count" ) +members = members[ :-1 ] +tbl = re.search( r'kNodeFieldNames\s*=\s*\{\s*\{(.*?)\}\s*\}\s*;', src, re.S ) +if tbl is None: sys.exit( "could not find kNodeFieldNames" ) +rows = re.findall( r'\{\s*"([^"\\]*)"\s*,\s*(\d+)\s*\}', tbl.group( 1 ) ) +if len( rows ) != len( members ): + sys.exit( "kNodeFieldNames has %d rows for %d enumerators" % ( len( rows ), len( members ) ) ) +for ( name, length ), member in zip( rows, members ): + if int( length ) != len( name ): + sys.exit( 'declared length %s != len("%s")' % ( length, name ) ) + print( "%s\t%s" % ( member, name ) ) +PYNAMES +NFIELDS="$( wc -l < "$NAMES" | tr -d ' ' )" +if [ "${NFIELDS:-0}" -lt 20 ]; then + no "harvested only ${NFIELDS:-0} field spellings from the header — every arm below would be vacuous" + echo; echo "SOME CHECKS FAILED"; exit 1 +fi +ok "harvested $NFIELDS field spellings from src/infra/fieldid.h (enumerator order == kNodeFieldNames order, declared lengths correct)" + +# ── harvest the grammar / extension table FROM THE TREE ────────────────────────────────────────────── +ROWS="$TMP/rows.txt" +python3 - "$CRAWL" > "$ROWS" <<'PYROWS' +import re, sys +src = open( sys.argv[1] ).read() +rows = re.findall( r'\{\s*"(\.[A-Za-z0-9_]+)"\s*,\s*Lang::\w+\s*,\s*&(tree_sitter_[A-Za-z0-9_]+)\s*,', src ) +for ext, fn in rows: + print( "%s\t%s" % ( ext, fn ) ) +PYROWS +NROWS="$( wc -l < "$ROWS" | tr -d ' ' )" +NGRAMMARS="$( cut -f2 "$ROWS" | sort -u | wc -l | tr -d ' ' )" +if [ "${NROWS:-0}" -lt 30 ] || [ "${NGRAMMARS:-0}" -lt 15 ]; then + no "harvested only ${NROWS:-0} extension rows / ${NGRAMMARS:-0} grammars from kLangTable — the probe found nothing" + echo; echo "SOME CHECKS FAILED"; exit 1 +fi +ok "harvested $NROWS extension rows over $NGRAMMARS distinct grammars from src/ingest_crawl.h's kLangTable" + +# ── F. capacity: every distinct grammar must FIT the registry ──────────────────────────────────────── +CAP="$( sed -n 's/.*kFieldIdCapacity *= *\([0-9]*\).*/\1/p' "$HDR" | head -1 )" +if [ -z "${CAP:-}" ]; then + no "F: could not read kFieldIdCapacity from the header" +elif [ "$NGRAMMARS" -gt "$CAP" ]; then + no "F: kLangTable names $NGRAMMARS distinct grammars but kFieldIdCapacity is $CAP — the overflow grammars silently keep the by-name path" +else + ok "F: $NGRAMMARS distinct grammars fit kFieldIdCapacity=$CAP" +fi + +# ── pick a fixture file per extension row, from the tree, excluding vendored sources ───────────────── +FIXTURES="$TMP/fixtures.txt" +: > "$FIXTURES" +while IFS="$( printf '\t' )" read -r ext fn; do + f="$( cd "$ROOT" && git ls-files "*$ext" 2>/dev/null | grep -v '^third_party/' | grep -v '^build/' \ + | while IFS= read -r c; do [ -f "$ROOT/$c" ] && [ "$( wc -c < "$ROOT/$c" | tr -d ' ' )" -lt 262144 ] && { printf '%s\n' "$c"; break; }; done )" + [ -n "${f:-}" ] && printf '%s\t%s\t%s\n' "$ext" "$fn" "$f" >> "$FIXTURES" +done < "$ROWS" +NFIX="$( wc -l < "$FIXTURES" | tr -d ' ' )" +NFIXG="$( cut -f2 "$FIXTURES" | sort -u | wc -l | tr -d ' ' )" +if [ "${NFIX:-0}" -lt 25 ] || [ "${NFIXG:-0}" -lt 15 ]; then + no "B: only ${NFIX:-0} extension rows (${NFIXG:-0} grammars) found a fixture file in the tree — arm B would be thin" +else + ok "B: $NFIX extension rows over $NFIXG grammars have a fixture file in the tree" +fi + +# ── the harness generator: shared by the real header and by the mutation copies ────────────────────── +# $1 = include dir holding the fieldid.h under test, $2 = output binary, $3 = compile log +gen_and_build(){ + local incdir="$1" out="$2" log="$3" + python3 - "$NAMES" "$ROWS" "$FIXTURES" > "$TMP/harness.cpp" <<'PYHARNESS' +import sys +names = [ l.rstrip( "\n" ).split( "\t" ) for l in open( sys.argv[ 1 ] ) if l.strip() ] +rows = [ l.rstrip( "\n" ).split( "\t" ) for l in open( sys.argv[ 2 ] ) if l.strip() ] +fixes = [ l.rstrip( "\n" ).split( "\t" ) for l in open( sys.argv[ 3 ] ) if l.strip() ] +grammars = sorted( { fn for _, fn in rows } ) + +print( '#include "fieldid.h"' ) +print( """ +#include +#include +#include +#include +#include + +static int failures = 0; +static void bad( const char* what, const char* g, const char* f, const char* extra ) +{ + if( ++failures <= 12 ) { std::printf( "MISMATCH %s grammar=%s field=%s %s\\n", what, g, f, extra ); } +} + +// The reference spelling of each field, harvested from a PRISTINE header at gate time. The table under +// test must never be consulted for these — that is the whole point of arm D. +struct Ref { const char* name; unsigned len; }; +""" ) +print( "static const Ref kRef[] = {" ) +for member, name in names: + print( ' { "%s", %d },' % ( name, len( name ) ) ) +print( "};" ) +print( "static const std::size_t kNRef = sizeof( kRef ) / sizeof( kRef[0] );" ) +print( 'static_assert( kNRef == rw::kNodeFieldCount, "reference table and NodeField disagree on the field count" );' ) + +print( 'extern "C" {' ) +for g in grammars: + print( "const TSLanguage* %s( void );" % g ) +print( "}" ) +print( "struct GrammarRow { const char* name; const TSLanguage* ( *fn )( void ); };" ) +print( "static const GrammarRow kGrammars[] = {" ) +for g in grammars: + print( ' { "%s", %s },' % ( g, g ) ) +print( "};" ) +print( "static const std::size_t kNGrammars = sizeof( kGrammars ) / sizeof( kGrammars[0] );" ) +print( "struct FixtureRow { const char* ext; const char* grammar; const TSLanguage* ( *fn )( void ); const char* path; };" ) +print( "static const FixtureRow kFixtures[] = {" ) +for ext, fn, path in fixes: + print( ' { "%s", "%s", %s, "%s" },' % ( ext, fn, fn, path ) ) +print( "};" ) +print( "static const std::size_t kNFixtures = sizeof( kFixtures ) / sizeof( kFixtures[0] );" ) + +print( """ +static std::string slurp( const char* path ) +{ + std::FILE* fp = std::fopen( path, "rb" ); + if( fp == nullptr ) { return std::string(); } + std::string out; + char buf[ 65536 ]; + std::size_t n = 0; + while( ( n = std::fread( buf, 1, sizeof( buf ), fp ) ) > 0 ) { out.append( buf, n ); } + std::fclose( fp ); + return out; +} + +int main( int argc, char** argv ) +{ + const char* root = ( argc > 1 ) ? argv[ 1 ] : "."; + + // ── A0: the UNWARMED path is the by-name answer ──────────────────────────────────────────────── + std::size_t coldPairs = 0; + for( std::size_t g = 0; g < kNGrammars; ++g ) + { + const TSLanguage* lang = kGrammars[ g ].fn(); + for( std::size_t f = 0; f < kNRef; ++f ) + { + const TSFieldId want = ts_language_field_id_for_name( lang, kRef[ f ].name, kRef[ f ].len ); + const TSFieldId got = rw::fieldIdFor( lang, static_cast< rw::NodeField >( f ) ); + if( want != got ) { bad( "A0", kGrammars[ g ].name, kRef[ f ].name, "cold lookup differs from by-name" ); } + ++coldPairs; + } + } + if( rw::warmedGrammarCount() != 0 ) { bad( "A0", "-", "-", "registry was not cold at start of run" ); } + std::printf( "ARM_A0 pairs=%zu\\n", coldPairs ); + + // ── A: warm every grammar, then the same equality, plus the registry population ──────────────── + for( std::size_t g = 0; g < kNGrammars; ++g ) { rw::warmFieldIds( kGrammars[ g ].fn() ); } + for( std::size_t g = 0; g < kNGrammars; ++g ) { rw::warmFieldIds( kGrammars[ g ].fn() ); } // idempotent + if( rw::warmedGrammarCount() != kNGrammars ) + { + std::printf( "MISMATCH A warmedGrammarCount=%zu expected=%zu (a warm was dropped, or warming is not idempotent)\\n", + rw::warmedGrammarCount(), kNGrammars ); + ++failures; + } + std::size_t warmPairs = 0, zeroIdPairs = 0; + for( std::size_t g = 0; g < kNGrammars; ++g ) + { + const TSLanguage* lang = kGrammars[ g ].fn(); + for( std::size_t f = 0; f < kNRef; ++f ) + { + const TSFieldId want = ts_language_field_id_for_name( lang, kRef[ f ].name, kRef[ f ].len ); + const TSFieldId got = rw::fieldIdFor( lang, static_cast< rw::NodeField >( f ) ); + if( want != got ) + { + char extra[ 96 ]; + std::snprintf( extra, sizeof( extra ), "table=%u by-name=%u", unsigned( got ), unsigned( want ) ); + bad( "A", kGrammars[ g ].name, kRef[ f ].name, extra ); + } + if( want == 0 ) { ++zeroIdPairs; } + ++warmPairs; + } + } + std::printf( "ARM_A pairs=%zu grammars=%zu fields=%zu zero_id_pairs=%zu\\n", warmPairs, kNGrammars, kNRef, zeroIdPairs ); + + // ── C non-vacuity: at least one ( grammar, field ) pair must resolve to id 0, or arm C proves + // nothing about the "grammar has no such field" case that 199 single-language sites rely on. + if( zeroIdPairs == 0 ) + { + std::printf( "MISMATCH C every grammar has every field — the unknown-field arm is vacuous\\n" ); + ++failures; + } + + // ── B + C: enumerated ( node, field ) pairs over real parse trees ────────────────────────────── + std::size_t nodePairs = 0, nodesWalked = 0, filesParsed = 0, nullAgreements = 0; + TSParser* parser = ts_parser_new(); + for( std::size_t i = 0; i < kNFixtures; ++i ) + { + const std::string path = std::string( root ) + "/" + kFixtures[ i ].path; + const std::string src = slurp( path.c_str() ); + if( src.empty() ) { continue; } + const TSLanguage* lang = kFixtures[ i ].fn(); + if( !ts_parser_set_language( parser, lang ) ) { continue; } + TSTree* tree = ts_parser_parse_string( parser, nullptr, src.data(), static_cast< std::uint32_t >( src.size() ) ); + if( tree == nullptr ) { continue; } + ++filesParsed; + TSTreeCursor cursor = ts_tree_cursor_new( ts_tree_root_node( tree ) ); + for( ;; ) + { + const TSNode n = ts_tree_cursor_current_node( &cursor ); + ++nodesWalked; + for( std::size_t f = 0; f < kNRef; ++f ) + { + const TSNode want = ts_node_child_by_field_name( n, kRef[ f ].name, kRef[ f ].len ); + const TSNode got = rw::fieldChild( n, static_cast< rw::NodeField >( f ) ); + const bool wn = ts_node_is_null( want ); + const bool gn = ts_node_is_null( got ); + if( wn != gn || ( !wn && !ts_node_eq( want, got ) ) ) + { + char extra[ 160 ]; + std::snprintf( extra, sizeof( extra ), "file=%s node=%s byname=%s fieldChild=%s", + kFixtures[ i ].path, ts_node_type( n ), + wn ? "(null)" : ts_node_type( want ), gn ? "(null)" : ts_node_type( got ) ); + bad( "B", kFixtures[ i ].grammar, kRef[ f ].name, extra ); + } + if( wn && gn ) { ++nullAgreements; } + ++nodePairs; + } + // depth-first over the WHOLE tree, named and anonymous alike + if( ts_tree_cursor_goto_first_child( &cursor ) ) { continue; } + for( ;; ) + { + if( ts_tree_cursor_goto_next_sibling( &cursor ) ) { break; } + if( !ts_tree_cursor_goto_parent( &cursor ) ) { goto doneTree; } + } + } + doneTree: + ts_tree_cursor_delete( &cursor ); + ts_tree_delete( tree ); + } + ts_parser_delete( parser ); + std::printf( "ARM_B files=%zu nodes=%zu pairs=%zu null_agreements=%zu\\n", filesParsed, nodesWalked, nodePairs, nullAgreements ); + if( filesParsed < 15 || nodePairs < 100000 ) + { + std::printf( "MISMATCH B only %zu files / %zu pairs walked — too thin to be evidence\\n", filesParsed, nodePairs ); + ++failures; + } + + // ── C: the id-0 contract, asserted directly rather than inferred from the walk ───────────────── + { + TSParser* p2 = ts_parser_new(); + ts_parser_set_language( p2, kGrammars[ 0 ].fn() ); + const char* tiny = "int f( int a ) { return a; }\\n"; + TSTree* t2 = ts_parser_parse_string( p2, nullptr, tiny, static_cast< std::uint32_t >( std::strlen( tiny ) ) ); + const TSNode r = ts_tree_root_node( t2 ); + if( !ts_node_is_null( ts_node_child_by_field_id( r, 0 ) ) ) + { + std::printf( "MISMATCH C ts_node_child_by_field_id( n, 0 ) is not the null node — the whole unknown-field contract rests on this\\n" ); + ++failures; + } + if( !ts_node_is_null( ts_node_child_by_field_name( r, "no_such_field_anywhere", 22 ) ) ) + { + std::printf( "MISMATCH C by-name lookup of an absent field is not null\\n" ); + ++failures; + } + ts_tree_delete( t2 ); + ts_parser_delete( p2 ); + } + + if( failures != 0 ) { std::printf( "FAILURES %d\\n", failures ); return 1; } + std::printf( "UNIT_OK\\n" ); + return 0; +} +""" ) +PYHARNESS + "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra \ + -I "$incdir" -I "$ROOT/third_party/deps/tree_sitter/lib/include" \ + "$TMP/harness.cpp" $GRAMMAR_OBJS "$TSLIB" -o "$out" 2>"$log" +} + +# ── A0/A/B/C against the real header ───────────────────────────────────────────────────────────────── +mkdir -p "$TMP/real" && cp "$HDR" "$TMP/real/fieldid.h" +if ! gen_and_build "$TMP/real" "$TMP/real.bin" "$TMP/real.cc.log"; then + no "harness does not compile/link against the real fieldid.h" + sed 's/^/ /' "$TMP/real.cc.log" | head -25 +else + if "$TMP/real.bin" "$ROOT" > "$TMP/real.out" 2>&1; then + grep -q UNIT_OK "$TMP/real.out" || no "harness exited 0 without UNIT_OK" + ok "A0: unwarmed fieldIdFor == ts_language_field_id_for_name — $( grep -o 'ARM_A0 pairs=[0-9]*' "$TMP/real.out" )" + ok "A: warm table == by-name resolution — $( sed -n 's/^ARM_A //p' "$TMP/real.out" )" + ok "C: id-0 is the null node, and the zero-id case is non-vacuous ($( sed -n 's/.*zero_id_pairs=\([0-9]*\).*/\1/p' "$TMP/real.out" ) grammar-field pairs have no such field)" + ok "B: fieldChild == ts_node_child_by_field_name on every node of every fixture — $( sed -n 's/^ARM_B //p' "$TMP/real.out" )" + else + no "A/B/C: harness failed" + sed 's/^/ /' "$TMP/real.out" | head -16 + fi +fi + +# ── D. the mutation arms — each must FAIL ──────────────────────────────────────────────────────────── +# D1: misspell ONE field's row in kNodeFieldNames. The table's entry for that field becomes a different +# id (usually 0) in every grammar, and arms A and B must both see it. The reference side is unmoved +# because the harness's literals came from the pristine header above. +mkdir -p "$TMP/mut1" +sed 's/{ "name", 4 }/{ "nane", 4 }/' "$HDR" > "$TMP/mut1/fieldid.h" +if ! grep -q '"nane"' "$TMP/mut1/fieldid.h"; then + no "D1: could not apply the misspelling mutation — kNodeFieldNames' row for \"name\" moved, so this arm proves nothing" +elif ! gen_and_build "$TMP/mut1" "$TMP/mut1.bin" "$TMP/mut1.cc.log"; then + no "D1: mutated header does not compile — the mutation must produce a WRONG build, not a broken one" +elif "$TMP/mut1.bin" "$ROOT" > "$TMP/mut1.out" 2>&1; then + no "D1: arms A/B stayed GREEN with one field's spelling corrupted in the table — they cannot fail" +else + ok "D1: arms A/B go red when one row of kNodeFieldNames is misspelled ($( sed -n 's/^FAILURES //p' "$TMP/mut1.out" ) disagreements)" +fi + +# D2: shift the field index inside the LOOKUP rather than the data. Every warm grammar then answers with +# its neighbour field's id — a defect no amount of checking the table's contents would catch. +mkdir -p "$TMP/mut2" +sed 's/return registry.ids\[ i \]\[ f \];/return registry.ids[ i ][ ( f + 1 ) % kNodeFieldCount ];/' "$HDR" > "$TMP/mut2/fieldid.h" +if ! grep -q 'f + 1 ) % kNodeFieldCount' "$TMP/mut2/fieldid.h"; then + no "D2: could not apply the index-shift mutation — fieldIdFor's return moved, so this arm proves nothing" +elif ! gen_and_build "$TMP/mut2" "$TMP/mut2.bin" "$TMP/mut2.cc.log"; then + no "D2: mutated header does not compile" +elif "$TMP/mut2.bin" "$ROOT" > "$TMP/mut2.out" 2>&1; then + no "D2: arms A/B stayed GREEN with the lookup reading the wrong field slot — they cannot fail" +else + ok "D2: arms A/B go red when the lookup reads the neighbouring field's id ($( sed -n 's/^FAILURES //p' "$TMP/mut2.out" ) disagreements)" +fi + +# ── E. population: the conversion is still the conversion ──────────────────────────────────────────── +# A FUNCTION OVER A DIRECTORY, not a straight-line grep of $ROOT, for the same reason nodekindcheck's +# arm D is: the mutation control has to run it against a REVERTED copy of the tree, and a gate that can +# only look at its own checkout cannot be shown to fail. Never mutate $ROOT itself. +checkPopulation(){ # $1 = tree root to inspect; prints one line per violation, empty = clean + local root="$1" f n k + for f in $SITE_FILES; do + [ -f "$root/$f" ] || { printf '%s: MISSING\n' "$f"; continue; } + n="$( grep -c 'ts_node_child_by_field_name' "$root/$f" 2>/dev/null || true )" + k="$( grep -c 'fieldChild(\|NodeField::' "$root/$f" 2>/dev/null || true )" + [ "${n:-0}" -ne 0 ] && printf '%s: %s ts_node_child_by_field_name site(s) left\n' "$f" "$n" + [ "${k:-0}" -lt 2 ] && printf '%s: only %s fieldChild/NodeField site(s) — not on the field-id table\n' "$f" "${k:-0}" + done + return 0 +} + +violations="$( checkPopulation "$ROOT" )" +if [ -n "$violations" ]; then + printf '%s\n' "$violations" | while IFS= read -r v; do no "E: $v"; done + fail=1 +else + TOTALSITES="$( cd "$ROOT" && grep -ho 'fieldChild(' $SITE_FILES | wc -l | tr -d ' ' )" + if [ "${TOTALSITES:-0}" -lt 150 ]; then + no "E: only ${TOTALSITES:-0} fieldChild( sites across the converted files — the 199-site conversion has been eroded" + else + ok "E: all $( echo $SITE_FILES | wc -w | tr -d ' ' ) converted files are on fieldChild ($TOTALSITES sites), with no ts_node_child_by_field_name left" + fi +fi + +# E-warm: the per-grammar warm must actually be wired into the ingest translation unit, over kLangTable. +checkWarmSite(){ # $1 = tree root; prints one line per violation, empty = clean + local root="$1" + grep -q 'warmFieldIds(' "$root/src/ingest_crawl.h" 2>/dev/null \ + || printf 'src/ingest_crawl.h: no warmFieldIds( ) call — nothing fills the table\n' + grep -q 'kLangTable' "$root/src/ingest_crawl.h" 2>/dev/null \ + || printf 'src/ingest_crawl.h: the warm does not walk kLangTable\n' + grep -q 'warmFieldIdTable( *)' "$root/src/ingest.cpp" 2>/dev/null \ + || printf 'src/ingest.cpp: ingest() never calls warmFieldIdTable() — every grammar takes the by-name fallback\n' + return 0 +} +warmViolations="$( checkWarmSite "$ROOT" )" +if [ -n "$warmViolations" ]; then + printf '%s\n' "$warmViolations" | while IFS= read -r v; do no "E-warm: $v"; done + fail=1 +else + ok "E-warm: the per-grammar warm is wired over kLangTable and called from ingest()" +fi + +# E-mut: the revert control. A scratch copy of the converted files is rewritten back to the by-name form +# — every `NodeField::X` to its spelling and every `fieldChild(` to `ts_node_child_by_field_name(` — and +# arm E's own function must report EVERY one of them. The count is of FILES reported, not of one +# violation shape, because the two shapes are file-dependent: a file whose sites are all direct reports +# "sites left", while src/slice.h (which reads fields through three wrappers over ONE fieldChild) reports +# the population floor instead. Requiring all nine files either way is what rules out a blind file. +mkdir -p "$TMP/reverted/src" +revertedFiles=0 +for f in $SITE_FILES; do + if python3 - "$ROOT/$f" "$TMP/reverted/$f" "$NAMES" <<'PYREVERT' +import re, sys +spell = dict( l.rstrip( "\n" ).split( "\t" ) for l in open( sys.argv[ 3 ] ) if l.strip() ) +src = open( sys.argv[ 1 ] ).read() +out, n1 = re.subn( r'NodeField::(\w+)', lambda m: '"%s"' % spell.get( m.group( 1 ), m.group( 1 ) ), src ) +out, n2 = re.subn( r'\bfieldChild\(', 'ts_node_child_by_field_name(', out ) +open( sys.argv[ 2 ], 'w' ).write( out ) +sys.exit( 0 if ( n1 + n2 ) > 0 else 1 ) +PYREVERT + then revertedFiles=$(( revertedFiles + 1 )); fi +done +NSITEFILES="$( echo $SITE_FILES | wc -w | tr -d ' ' )" +if [ "$revertedFiles" -ne "$NSITEFILES" ]; then + no "E-mut: could only build a reverted copy of $revertedFiles of $NSITEFILES converted file(s) — the control cannot prove arm E fires" +else + mutFiles="$( checkPopulation "$TMP/reverted" | cut -d: -f1 | sort -u | wc -l | tr -d ' ' )" + if [ "${mutFiles:-0}" -eq "$NSITEFILES" ]; then + ok "E-mut: arm E goes red on a full revert of the change — all $mutFiles file(s) reported (it is not green in both directions)" + else + no "E-mut: a full revert left arm E green on $(( NSITEFILES - ${mutFiles:-0} )) of $NSITEFILES file(s) — the arm has a blind file" + fi +fi + +# E-warm-mut: deleting the warm call must make E-warm report it. +mkdir -p "$TMP/nowarm/src" +cp "$ROOT/src/ingest_crawl.h" "$TMP/nowarm/src/ingest_crawl.h" +sed 's/warmFieldIdTable( *)/\/* removed by E-warm-mut *\//' "$ROOT/src/ingest.cpp" > "$TMP/nowarm/src/ingest.cpp" +if grep -q 'warmFieldIdTable( *)' "$TMP/nowarm/src/ingest.cpp"; then + no "E-warm-mut: could not remove the warm call from a scratch copy — the control proves nothing" +elif [ -z "$( checkWarmSite "$TMP/nowarm" )" ]; then + no "E-warm-mut: E-warm stayed green with the warm call deleted — the arm cannot fail" +else + ok "E-warm-mut: E-warm goes red when ingest() stops calling warmFieldIdTable()" +fi + +echo +if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi diff --git a/test/regression.sh b/test/regression.sh index 0d54307d8..cb15623dc 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 540938e2c9155b0138d5b638c686649253c1f46a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:24:49 -0400 Subject: [PATCH 11/73] =?UTF-8?q?feat(handoff):=20the=20continuation=20pac?= =?UTF-8?q?ket=20showed=20one=20symbol=20in=20six=20of=20what=20changed=20?= =?UTF-8?q?=E2=80=94=206=20becomes=2050=20code=20/=2012=20prose?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kHandoffSymbolsPerFile = 6 cut the DISK-TRUTH half of the packet, the section whose whole contract is "this is what the change set is", and it fired on the typical case rather than a tail. Measured over 12 real commits replayed as working-tree diffs (39 changed files, 1,406 symbols): the cap fired on 27 of 39 files — 69% — and per-commit containment ran 6.6% / 8.4% / 10.0% / 14.6% / 17.8% / 21.6% / 46.2% / 79.2% / 80.0% / 100%. TWO CAPS, NOT ONE. A single number is decided by the wrong population. 26 of those 39 files were markdown carrying 1,009 of the 1,406 symbols (72%), and the whole heavy tail is documentation: docs/EVALS.md 217 sections, docs/LIMITS.md 55, README.md 35. Code: p50=21, p90=57, tail src/mcpverbs.h 104. A uniform 50 costs +51.4% bytes for 59.0% containment; the split buys more of the half that matters for a quarter of the price, because a continuation packet wants a document's NAME and first sections, not its table of contents. kHandoffSymbolsPerCodeFile 6 -> 50 just under the code p90 of 57 kHandoffSymbolsPerDocFile 6 -> 12 A/B, both binaries built from this tree, 37 real commits of this repository replayed as working-tree diffs (72 changed files, 2,179 symbols): cap 6 50/12 symbols shown 354 1175 of 2179 that exist containment 16.2% 53.9% files cut 51 15 of 72 bytes 121,043 135,434 (+14,391, +11.9%; +388 B per handoff) a narrower, code-heavy window (13 commits / 23 files): 32.4% -> 81.4%, +191 B per handoff (+6.0%) PURELY ADDITIVE, which is the owner's bar for a raise: over both windows, 0 rows removed, 0 rows replaced (every old row is a prefix of the new one), 0 non-disclosure attribute changes. The only attribute movement is syms_capped/syms_total RETIRING as the cap stops firing, which is the disclosure behaving correctly. --token-budget is unaffected in mechanism and honest in its labels: on the same diff the verified floor grows 1360 -> 1417 est_tokens, --token-budget=3000 is still honoured, and --token-budget=500 still reports over_ceiling="1" with the verified rows intact. The legend clause is now FORMATTED from the two constants instead of carrying the literal "6" in its sentence — it had already gone stale once by construction, and a legend naming a value it does not read is the one-number-in-six-artifacts shape. GATE. test/tracehandoffcapcheck.sh arm (B) sized its fixture with a literal 12-against-6: at a cap of 50 that fixture tests NOTHING and still prints PASS. It now reads both caps out of src/handoff.h and computes four files from them — wide.c at cap+10 functions, narrow.c at 3, wide.md at cap+10 sections, narrow.md at 2 — so the fixture can never fall back under the cap it is checking. Six assertions became seven: crossing/disclosure/silence for EACH cap, plus B7, that the two caps differ and the prose file is cut at the smaller one — the one assertion a build ignoring the split cannot satisfy. Red-proven against the pre-change binary (RIPWIRE_BIN=): FAIL B1 crossing: packet lists 6 of the map's 61 symbols in wide.c (cap 50) FAIL B4 crossing: packet lists 6 of the map's 25 symbols in wide.md (cap 12) FAIL B7 split: the two caps differ (code 50, prose 12) … docs/limits_build.py's NAME filter reads `Per\w*File`, not the compound `PerFile`: a filter that turns on an exact spelling is the defect the widening exists to remove, not a smaller instance of it. docs/LIMITS.md and docs/limits_classes.tsv regenerated; both new caps classified OUTPUT. Green: tracehandoffcapcheck, handoffcheck, budgetpolicycheck, emittertruthcheck, printffmtparitycheck (42 verbs byte-identical), limitstablecheck. --- docs/LIMITS.md | 17 +++--- docs/limits_build.py | 6 +- docs/limits_classes.tsv | 3 +- src/handoff.h | 85 ++++++++++++++++++++++------- test/tracehandoffcapcheck.sh | 103 ++++++++++++++++++++++++----------- 5 files changed, 152 insertions(+), 62 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index d2163a7dd..dc3a655eb 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -10,10 +10,10 @@ where the pathological tail is, never near the typical case — and when it fire | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 205 | 81 | 99 | **106** | +| 206 | 81 | 100 | **106** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 205 + 7 is the 212 constants this generator parses out of `src/`. +are not counted as caps, and 206 + 7 is the 213 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -31,8 +31,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. **108 of 205 caps are classified -(37 INDEXING, 36 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **109 of 206 caps are classified +(37 INDEXING, 37 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 @@ -340,10 +340,11 @@ Discloses: `syms_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kHandoffCochangeRows` | `8` | 43 | OUTPUT | heuristic co-change rows shown | -| `kHandoffDocRows` | `4` | 41 | OUTPUT | heuristic doc pointers shown | -| `kHandoffNoteRows` | `8` | 42 | OUTPUT | heuristic note rows shown | -| `kHandoffSymbolsPerFile` | `6` | 50 | OUTPUT | — | +| `kHandoffCochangeRows` | `8` | 44 | OUTPUT | heuristic co-change rows shown | +| `kHandoffDocRows` | `4` | 42 | OUTPUT | heuristic doc pointers shown | +| `kHandoffNoteRows` | `8` | 43 | OUTPUT | heuristic note rows shown | +| `kHandoffSymbolsPerCodeFile` | `50` | 79 | OUTPUT | — | +| `kHandoffSymbolsPerDocFile` | `12` | 80 | OUTPUT | — | ### `src/infra/blanktext.h` diff --git a/docs/limits_build.py b/docs/limits_build.py index 03b22a2e9..1929bccd8 100644 --- a/docs/limits_build.py +++ b/docs/limits_build.py @@ -36,7 +36,11 @@ DECL = re.compile(r'^[ \t]*(?:static[ \t]+)?(?:inline[ \t]+)?constexpr[ \t]+[\w:<>, ]*?' r'\b(k[A-Z][A-Za-z0-9_]*)[ \t]*=[ \t]*(?:\r?\n[ \t]*)?([0-9][0-9_.eE+-]*)[ \t]*;(.*)$', re.M) -KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width|Shown|PerFile|Hits') +KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width|Shown|Hits' + r'|Per\w*File') # Per\w*File, not PerFile: kHandoffCodeSymbolsPerFile and + # kHandoffSymbolsPerCodeFile are the same kind of cap, and a + # NAME filter that turns on a compound spelling is the defect + # this widening exists to remove, not a smaller instance of it. # A CAP answers "how many of X survive". A HYPERPARAMETER answers "how is X weighted or apportioned". # They are not the same instrument and must not share a table: a cap is judged by what it truncates and diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index 42831f6e0..a6e93df46 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -38,7 +38,8 @@ kFieldWalkCap INDEXING kHandoffCochangeRows OUTPUT kHandoffDocRows OUTPUT kHandoffNoteRows OUTPUT -kHandoffSymbolsPerFile OUTPUT +kHandoffSymbolsPerCodeFile OUTPUT +kHandoffSymbolsPerDocFile OUTPUT kIdiomMaxCondTokens BOUNDARY kIdiomMaxLabelTokens BOUNDARY kIdiomMaxReturnTokens BOUNDARY diff --git a/src/handoff.h b/src/handoff.h index 77c26aae4..4ed47930c 100644 --- a/src/handoff.h +++ b/src/handoff.h @@ -31,6 +31,7 @@ #include "docparse.h" // isIndexedDocExtension / lowerExtOf — the shared prose vocabulary #include #include +#include #include #include #include @@ -41,13 +42,42 @@ namespace rw inline constexpr std::size_t kHandoffDocRows = 4; // heuristic doc pointers shown inline constexpr std::size_t kHandoffNoteRows = 8; // heuristic note rows shown inline constexpr std::size_t kHandoffCochangeRows = 8; // heuristic co-change rows shown -// verified symbols listed per changed file. This one cuts the DISK-TRUTH half of the packet — the section whose -// whole contract is "this is what the change set is" — and it fires on the TYPICAL case, not a tail: the -// 2026-09-10 cap round measured an ordinary two-file source diff listing 12 symbols out of 44 (73% withheld) -// beside no marker at all. The row it cuts now carries syms_total= and syms_capped="1"; an under the -// cap carries neither, so an uncut packet is byte-identical. The VALUE is unchanged — disclosing a cut and -// raising it are separate deliverables with separate evidence. -inline constexpr std::size_t kHandoffSymbolsPerFile = 6; +// Verified symbols listed per changed file. This cuts the DISK-TRUTH half of the packet — the section whose +// whole contract is "this is what the change set is" — and at 6 it fired on the TYPICAL case, not a tail. +// +// MEASURED, 2026-09-10, over 12 real commits of this repository replayed as working-tree diffs (39 changed +// files, 1,406 symbols; the frozen capsweep corpus cannot see this cap at all, so the ladder was run against +// real history). At 6 the cap fired on 27 of 39 changed files — 69% — and per-commit containment ran +// 6.6% / 8.4% / 10.0% / 14.6% / 17.8% / 21.6% / 46.2% / 79.2% / 80.0% / 100%: on the typical commit the +// packet showed one symbol in six of what actually changed. +// +// cap files cut symbols shown / 1406 containment bytes over 12 handoffs +// 6 27 222 15.8% 46,993 +// 12 21 363 25.8% 52,021 (+10.7%) +// 25 15 565 40.2% 58,823 (+25.2%) +// 50 8 830 59.0% 71,139 (+51.4%) +// 100 3 1051 74.8% 82,662 (+75.9%) +// inf 0 1406 100.0% 113,472 (+141%) +// +// TWO CAPS, NOT ONE, because a single number is decided by the wrong population. 26 of the 39 changed files +// were markdown carrying 1,009 of the 1,406 symbols (72%), and the entire heavy tail is documentation: +// docs/EVALS.md 217 sections, docs/LIMITS.md 55, README.md 35. Code files: p50=21, p90=57, tail +// src/mcpverbs.h 104. A uniform cap set at the doc tail buys a +141% packet, 72% of it section titles; +// a uniform cap set at the code p90 spends most of its budget on markdown anyway. Split by kind: +// +// code (.h/.cpp/.py/.js/.sh): 6 -> 50 containment 16.6% -> 83.6%, and 50 is just under the code p90 of 57 +// docs (markdown/prose): 6 -> 12 containment 15.5% -> 24.2%; a continuation packet wants the doc's +// NAME and its first sections, not its table of contents +// +// The p99 rule does not survive contact with this sample: with n=39, p95, p99 and max are all 217 — one +// file, docs/EVALS.md, appearing in three of the twelve commits. p90 is the highest quantile it supports. +// +// PURELY ADDITIVE at every rung of every commit: across all 60 rung transitions, 0 rows removed, 0 rows +// replaced, 0 attribute changes other than syms_capped/syms_total RETIRING as the cap stops firing. +// The row that is cut carries syms_total= and syms_capped="1"; an under the cap carries neither, +// so a packet whose every file fits is byte-identical to the pre-disclosure one. +inline constexpr std::size_t kHandoffSymbolsPerCodeFile = 50; +inline constexpr std::size_t kHandoffSymbolsPerDocFile = 12; namespace handoff_detail { @@ -67,7 +97,15 @@ inline bool isIndexedDocPath( std::string_view p ) noexcept return docparse::isIndexedDocExtension( docparse::lowerExtOf( p ) ); } -// ONE changed file's row, and whether kHandoffSymbolsPerFile cut it. +// Which of the two caps a changed file is judged by. isIndexedDocPath is the SAME vocabulary the brief +// already uses to decide what counts as a design document (docparse.h), so a prose format the crawl learns +// gets the prose cap on the same day — rather than a private extension list here that drifts from it. +inline std::size_t symbolsPerFileCap( std::string_view pathRel ) +{ + return isIndexedDocPath( pathRel ) ? kHandoffSymbolsPerDocFile : kHandoffSymbolsPerCodeFile; +} + +// ONE changed file's row, and whether its cap cut it. // // Counting and emitting share the SINGLE pass the row loop always made: the old loop stopped AT the cap, so // it could not say how many symbols it had not reached, and the packet's disk-truth half showed six names @@ -84,8 +122,9 @@ struct VerifiedFileRow inline VerifiedFileRow verifiedFileRow( const IngestResult& ing, std::uint32_t fileId, std::string_view pathRel, std::vector& esc ) { - std::string shownRows; - std::uint32_t total = 0; + std::string shownRows; + std::uint32_t total = 0; + const std::size_t cap = symbolsPerFileCap( pathRel ); for( const Symbol& sym : ing.symbols ) { if( sym.fileId != fileId ) @@ -93,7 +132,7 @@ inline VerifiedFileRow verifiedFileRow( const IngestResult& ing, std::uint32_t f continue; } ++total; - if( total > kHandoffSymbolsPerFile ) + if( total > cap ) { continue; // counted, not shown — that gap is what syms_total= names } @@ -104,7 +143,7 @@ inline VerifiedFileRow verifiedFileRow( const IngestResult& ing, std::uint32_t f // serialize.h's ONE economy-of-attributes idiom: empty when the file's whole symbol set fits, so an uncut // stays byte-identical to the pre-disclosure packet. - const std::string capAttr = countFieldIfAbove( total, std::uint32_t( kHandoffSymbolsPerFile ), + const std::string capAttr = countFieldIfAbove( total, std::uint32_t( cap ), " syms_total=\"", "\" syms_capped=\"1\"" ); std::string row = " row was actually cut — tracelocus.h's hopLegendOf seam, so a -// packet whose every changed file fits under kHandoffSymbolsPerFile stays byte-identical to the pre-disclosure -// one. Angle brackets are entity-escaped because this text rides inside an XML comment, like the tail below. -inline constexpr const char* kHandoffSymsCapClause = - "<f> lists at most 6 of a changed file's symbols; syms_total= is how many that file actually defines and " - "syms_capped=\"1\" says the list was cut to the first 6 the index holds - both absent on the files that fit, " - "so an <f> without them is the WHOLE set and not a floor. "; +// packet whose every changed file fits under its cap stays byte-identical to the pre-disclosure one. Angle +// brackets are entity-escaped because this text rides inside an XML comment, like the tail below. +// +// The two numbers are FORMATTED from the constants, not typed into the sentence: a legend that names a value +// it does not read is the one-number-six-artifacts shape, and this one already went stale once when the cap +// moved off 6. +inline std::string handoffSymsCapClause() +{ + return std::format( "<f> lists at most {} of a changed CODE file's symbols and {} of a prose file's; " + "syms_total= is how many that file actually defines and syms_capped=\"1\" says the list " + "was cut to that many - both absent on the files that fit, so an <f> without them " + "is the WHOLE set and not a floor. ", + kHandoffSymbolsPerCodeFile, kHandoffSymbolsPerDocFile ); +} inline constexpr const char* kHandoffLegendTail = " is labeled non-verified suggestion (cochange=usually-edited-together deg=degree, note=committed " ".ripwire_notes row, doc=plan/design pointer s=lexical score for the branch+commit-subject query). " @@ -342,7 +389,7 @@ inline int writeHandoffPacket( std::FILE* out, const std::string& root, const In { std::string doc = kHandoffLegendHead; doc += rw::kRunHintLegendClause; // M21(b): the ONE wording, spliced — never a seventh paraphrase - if( anySymsCapped ) { doc += kHandoffSymsCapClause; } // empty unless an row was cut + if( anySymsCapped ) { doc += handoffSymsCapClause(); } // absent unless an row was cut doc += kHandoffLegendTail; doc += " rows inside each — the +# (B) src/handoff.h's symbols-per-file caps cut the rows inside each — the # DISK-TRUTH half of a continuation packet, the section whose whole contract is "this is what the # change set is". It was silent, and it fires on the typical case, not a tail: a two-file diff of # ordinary source files listed 12 symbols out of 44 (73% withheld) beside no marker at all. Now the @@ -155,21 +155,37 @@ grep -q PROBE_BROKEN "$TMP/a.res" && no "(A) --from-trace probe broken: $( cat " # =================================================================================================== # (B) the verified symbols-per-file rows — --handoff / kHandoffSymbolsPerFile # =================================================================================================== -echo "-- (B) verified symbols-per-file cap (kHandoffSymbolsPerFile, src/handoff.h)" +echo "-- (B) verified symbols-per-file caps (kHandoffSymbolsPerCodeFile / PerDocFile, src/handoff.h)" HFIX="$TMP/hfix" mkdir -p "$HFIX" -# wide.c is written PAST the cap by computation, not by eye; narrow.c stays comfortably under it. -python3 - "$HFIX" <<'PY' -import os, sys -d = sys.argv[1] -CAP = 6 -wide = "".join("int wideFn%02d( int a )\n{\n return a + %d;\n}\n" % (i, i) for i in range(12)) -assert wide.count("int wideFn") > CAP -narrow = "".join("int narrowFn%02d( int a )\n{\n return a - %d;\n}\n" % (i, i) for i in range(3)) -assert narrow.count("int narrowFn") < CAP +# The cap is TWO caps since 2026-09-10 — 50 for a code file, 12 for a prose file — so the fixture needs +# four files, and each of the four sizes is COMPUTED from the value read out of src/handoff.h. A fixture +# with a literal count is a fixture that silently stops crossing the cap the day the cap moves, which is +# exactly what happened here: the old one wrote 12 functions against a cap of 6, and at 50 it tests +# nothing at all while still printing PASS. +python3 - "$HFIX" "$ROOT/src/handoff.h" <<'PY' +import os, re, sys +d, hdr = sys.argv[1], open(sys.argv[2], encoding="utf-8").read() +def capOf(name): + m = re.search(r'\b%s\s*=\s*(\d+)\s*;' % name, hdr) + if not m: + sys.exit("tracehandoffcapcheck: %s is not declared in src/handoff.h — the fixture cannot size itself" % name) + return int(m.group(1)) +CODE, DOC = capOf("kHandoffSymbolsPerCodeFile"), capOf("kHandoffSymbolsPerDocFile") +open(os.path.join(d, "caps.txt"), "w").write("%d %d\n" % (CODE, DOC)) +wide = "".join("int wideFn%03d( int a )\n{\n return a + %d;\n}\n" % (i, i) for i in range(CODE + 10)) +assert wide.count("int wideFn") > CODE +narrow = "".join("int narrowFn%03d( int a )\n{\n return a - %d;\n}\n" % (i, i) for i in range(3)) +assert narrow.count("int narrowFn") < CODE +wided = "# Wide doc\n\n" + "".join("## section %03d\n\nbody\n\n" % i for i in range(DOC + 10)) +assert wided.count("## section") > DOC +narrowd = "# Narrow doc\n\n" + "".join("## section %03d\n\nbody\n\n" % i for i in range(2)) +assert narrowd.count("## section") < DOC open(os.path.join(d, "wide.c"), "w").write(wide) open(os.path.join(d, "narrow.c"), "w").write(narrow) +open(os.path.join(d, "wide.md"), "w").write(wided) +open(os.path.join(d, "narrow.md"), "w").write(narrowd) PY ( cd "$HFIX" || exit 1 @@ -182,15 +198,17 @@ PY # a real, ordinary edit to BOTH files — this is the diff the packet reports on printf 'int wideFnEdited( int a )\n{\n return a;\n}\n' >> "$HFIX/wide.c" printf 'int narrowFnEdited( int a )\n{\n return a;\n}\n' >> "$HFIX/narrow.c" +printf '\n## edited section\n\nbody\n' >> "$HFIX/wide.md" +printf '\n## edited section\n\nbody\n' >> "$HFIX/narrow.md" run "$HFIX" --handoff > "$TMP/b_handoff.xml" run "$HFIX" > "$TMP/b_map.xml" -python3 - "$TMP/b_handoff.xml" "$TMP/b_map.xml" <<'PY' > "$TMP/b.res" 2>&1 +python3 - "$TMP/b_handoff.xml" "$TMP/b_map.xml" "$HFIX/caps.txt" <<'PY' > "$TMP/b.res" 2>&1 import re, sys packet = open(sys.argv[1], encoding="utf-8", errors="replace").read() mapdoc = open(sys.argv[2], encoding="utf-8", errors="replace").read() -CAP = 6 +CODE, DOC = (int(x) for x in open(sys.argv[3], encoding="utf-8").read().split()) def files_of(doc): out = {} @@ -205,28 +223,47 @@ if not ver: print("PROBE_BROKEN no in packet: %r" % packet[-400:]); raise SystemExit pk = files_of(ver.group(0)) mp = files_of(mapdoc) -if "wide.c" not in pk or "narrow.c" not in pk or "wide.c" not in mp: +need = ["wide.c", "narrow.c", "wide.md", "narrow.md"] +if any(f not in pk for f in need) or "wide.c" not in mp or "wide.md" not in mp: print("PROBE_BROKEN packet=%s map=%s" % (sorted(pk), sorted(mp))); raise SystemExit -w_attrs, w_shown = pk["wide.c"] -n_attrs, n_shown = pk["narrow.c"] -_, w_real = mp["wide.c"] - -# 1. CROSSING — the file really has more symbols than the packet lists, per the tool's OWN map. -print("B1 %s crossing: packet lists %d of the map's %d symbols in wide.c (cap %d)" - % ("OK" if w_shown == CAP and w_real > CAP else "NO", w_shown, w_real, CAP)) - -# 2. DISCLOSURE — the cut says it was cut, and names the true total. -cap = re.search(r'\bsyms_capped="([^"]*)"', w_attrs) -tot = re.search(r'\bsyms_total="([^"]*)"', w_attrs) -good = cap and cap.group(1) == "1" and tot and int(tot.group(1)) == w_real -print("B2 %s disclosure: syms_capped=%s syms_total=%s (want 1 / %d)" - % ("OK" if good else "NO", cap.group(1) if cap else "", tot.group(1) if tot else "", w_real)) - -# 3. SILENCE — the uncut pays nothing. -print("B3 %s silence: narrow.c shows %d symbols, attrs=%r" - % ("OK" if ("syms_capped" not in n_attrs and "syms_total" not in n_attrs and n_shown < CAP) else "NO", - n_shown, n_attrs.strip())) +def crossing(tag, path, cap): + attrs, shown = pk[path] + _, real = mp[path] + print("%s %s crossing: packet lists %d of the map's %d symbols in %s (cap %d)" + % (tag, "OK" if shown == cap and real > cap else "NO", shown, real, path, cap)) + return attrs, real + +def disclosure(tag, path, real): + attrs, _ = pk[path] + cap = re.search(r'\bsyms_capped="([^"]*)"', attrs) + tot = re.search(r'\bsyms_total="([^"]*)"', attrs) + good = cap and cap.group(1) == "1" and tot and int(tot.group(1)) == real + print("%s %s disclosure on %s: syms_capped=%s syms_total=%s (want 1 / %d)" + % (tag, "OK" if good else "NO", path, cap.group(1) if cap else "", + tot.group(1) if tot else "", real)) + +def silence(tag, path, cap): + attrs, shown = pk[path] + good = "syms_capped" not in attrs and "syms_total" not in attrs and shown < cap + print("%s %s silence: %s shows %d symbols, attrs=%r" % (tag, "OK" if good else "NO", path, shown, attrs.strip())) + +# 1-3. THE CODE CAP, on a file written past it by computation. +_, w_real = crossing("B1", "wide.c", CODE) +disclosure("B2", "wide.c", w_real) +silence("B3", "narrow.c", CODE) + +# 4-6. THE PROSE CAP, which is a DIFFERENT number. Without a doc file in the fixture the split is +# untested: a build that ignored kHandoffSymbolsPerDocFile entirely would pass B1-B3 unchanged. +_, d_real = crossing("B4", "wide.md", DOC) +disclosure("B5", "wide.md", d_real) +silence("B6", "narrow.md", DOC) + +# 7. THE SPLIT ITSELF. A code file with MORE symbols than the doc cap but fewer than the code cap must +# be uncut, which is the one assertion a single-cap build cannot satisfy. +_, mid_real = mp["wide.c"], mp["wide.c"][1] +print("B7 %s split: the two caps differ (code %d, prose %d) and the prose file is cut at the SMALLER one" + % ("OK" if CODE != DOC and pk["wide.md"][1] == DOC and pk["wide.c"][1] == CODE else "NO", CODE, DOC)) PY while read -r tag verdict rest; do [ "$verdict" = OK ] && ok "$tag $rest" || no "$tag $rest" From 33ea14997f19a09204f0f3b69e5d4ae608d27b0c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:25:48 -0400 Subject: [PATCH 12/73] perf(lexical): the tokenizer, the BM25 scan's inner loop and its file read, all off the per-byte path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three of the audit's ranked findings, in the one place they all live — the pass-2 BM25 scan, which is 81.7% of `--pack-task`'s busy time and reads every doc-comment and body byte of the corpus. S1/S2 — THE TOKENIZER (src/lexindex.h). forEachLexSubtoken and forEachLexSubtokenHashed were TWO hand-kept copies of one per-byte state machine (the shape the file's own header warns about: the 2026-08-19 acronym bug lived in one copy and not the other). Both are now thin callers of a single block walk over rw::strkern::classMasks, 16 bytes at a time on NEON and 32 on AVX2. The boundary rule is unchanged and is now stated as mask algebra, derived in the header and PROVEN by gate: inToken(k) == A[k-1] (the walker sets a start at every alnum byte and clears it at every separator, so "in a token" IS "the previous byte was alnum" — and prevUpper is U[k-1] wherever A[k-1] already gates the term) split = U & (A<<1) & ( ~(U<<1) | (L>>1) ) starts = ( A & ~(A<<1) ) | split cuts = starts | ~A a token runs from each start to the next cut Three carries cross each block seam (A and U of the byte before, L of the byte after) and a `pending` start carries a token across any number of blocks. lexUpperOpensToken is gone from shipped code: the algebra is the rule now, and a second statement of it is the drift risk. The fused rolling hash is gone with the second state machine; the hash runs over the token's bytes after its span is known, with the branchless S2 fold `c | ( ( c & 0x40 ) >> 1 )` — EXACT for [A-Za-z0-9] (digits carry no 0x40 bit) and only for it, which is why lexSubtokenHash keeps the general range-tested form for callers that have not classified their bytes. P2-3 — THE MATCH LOOP (src/lexical.h). The scan ran the WHOLE match table for every corpus subtoken: linear in a table that grows with the query and doubles again under RIPWIRE_QSTEM. LexHeadIndex precomputes, once per query, a 256-bit set of the table's head bytes (a strkern::Byteset256 — the type the header already owns, not a second bitmap) and the rows bucketed by length, CSR-style. A corpus token whose head is absent touches no string at all. The surviving predicate is character for character the original one — length, head, memcmp fast path, lexTokenEqualsLowered acronym fallback, ascending m — so byte-identity is structural. P2-4 — THE FILE READ (src/lexical.h). lexicalScanText read every file through ifstream + ostringstream << rdbuf() + str(): two full copies per file. Now docparse::detail::readWholeFile, the documented never-re-roll helper, whose clear-on-failure is exactly the empty-string "skip this file" contract that was already there. and leave the header with it. THE OTHER EIGHT rdbuf() SITES, audited, not converted (none is on a per-byte or per-file hot path; each reads ONE file, once, per invocation): src/recall.h:74 the maxBytes==0 arm of a bounded reader whose other arm needs the stream src/recall.h:593 one --recall doc body src/recall.h:1238 one section-granular body src/eval.h:233 one eval fixture, harness-only src/eval.h:612 one symbol's file during eval scoring, harness-only src/skilleval.h:74 one SKILL.md src/skillscan.h:757 one .mcp.json src/skillscan.h:797 one skill manifest NON-DEGRADATION. 18/18 outputs byte-identical, before vs after, over three corpora (a frozen git-archive of this tree, the go corpus, rocksdb) x six verbs (--top-k=100000 default map, --for conceptual, --for name-exact, --grep, --pack-task, --recall). Determinism (two runs cmp) and xmllint clean. Gates green: strkerncheck (13 arms, NEON + the AVX2 mirror under Rosetta 2, mutation reds 10 arms), subtokencheck, postingscheck, bm25check, querycheck, recallparitycheck, xmlwellformed, plus every test --test-gate named (adaptivecutshape, includeprecise, rustimportprecise, fixedbufsweep, optremarks). --quality-delta: 5 gating rows on the first cut, 4 of them fixed rather than acked — lexicalScoresTiered's complexity (431->434) and verbosity (918->929) by moving the head index and the match predicate out to namespace scope (both now BELOW baseline), and a duplication pair by making the head set a strkern::Byteset256 instead of a second hand-rolled bitmap. What remains is one short-horizon-churn row with churn="self" on lexicalScanText — it says "you edited this symbol", which no edit can make untrue, and it is the exact labelled-noise class Q1's E11 dial proposes to stop gating. Not acked: an ack would hide a row that is honest about a real edit. --- src/lexical.h | 155 ++++++++++++++++++++++---- src/lexindex.h | 229 +++++++++++++++++++++++---------------- test/strkern_harness.cpp | 60 +++++----- 3 files changed, 298 insertions(+), 146 deletions(-) diff --git a/src/lexical.h b/src/lexical.h index 778d53479..6ce4915ff 100644 --- a/src/lexical.h +++ b/src/lexical.h @@ -9,12 +9,14 @@ // and BODY text, so a query matches code by what it DOES, not just what it's named. Deterministic. #include "model.h" +#include "docparse.h" // detail::readWholeFile — THE canonical whole-file byte read (P2-4); never re-rolled #include "lexindex.h" // B0: the ONE subtoken state machine + docCommentStart + persisted-stats types #include "sarif.h" // rootRelativeUri — the ONE root-relative path view, included directly // rather than reached transitively (recall.h gets it via serialize.h) // because pass 1.5 SCORES the string recall.h PRINTS. A pure path helper // over model.h despite the header's name: no cycle. #include "infra/profileScope.h" // PROFILE_SCOPE self-profiling — gated by PROFILE_ENABLED (off unless -DRIPWIRE_PROFILE=ON) +#include "infra/strkern.h" // Byteset256 — the head set below is that type, not a second bitmap #include "infra/sortutil.h" // deterministic sanitizer-clean score sorting for adaptive cuts #include @@ -24,9 +26,7 @@ #include #include #include -#include #include -#include #include #include #include @@ -167,6 +167,124 @@ inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexc // defined with the LB-2 anchor-plausibility machinery below; the LB-3 variant guard reuses the bound inline std::uint32_t routeCarrierCap( const IngestResult& ing ) noexcept; +// ── P2-3: the query-side head mask + length buckets (PLAN_FULL_AUDIT_2026-09-10) ───────────────────── +// +// The BM25 pass-2 scan is the hottest loop in the tool — 81.7% of `--pack-task`'s busy time — because for +// EVERY subtoken of the corpus it walked the WHOLE query match table, string-comparing as it went. That +// inner loop is linear in the table, and the table grows with the question (and doubles again when +// RIPWIRE_QSTEM arms its stem variants), so a longer query cost quadratically more for no retrieval gain. +// +// Almost every one of those comparisons was decidable from two bytes of metadata. A corpus token can only +// match a table row of the SAME LENGTH whose FIRST byte agrees. Both facts are precomputed once per query: +// +// headBits a 256-bit set of the table's (already lowercased) first bytes. A corpus token whose head is +// not in the set touches NO string at all — one shift and one test, and the overwhelmingly +// common answer is "no". +// bucketIdx the rows grouped by token length, CSR-style (bucketOff[len] .. bucketOff[len+1]), in +// ascending row order. A surviving token iterates only the rows that CAN match it. +// +// Byte-identity is structural, not measured-and-hoped. The surviving predicate at the call site is +// character for character the one that was there before (the memcmp fast path AND the +// lexTokenEqualsLowered acronym fallback, in that order), the rows are visited in ascending m exactly as +// the linear scan visited them, and the table's strings are distinct so at most one row can ever match. +// What changed is only WHICH rows are looked at, and every skipped row is one the old predicate would +// have rejected on its length or its head. +// +// kMaxLen is a BUCKETING BOUND, never an answer bound: a row longer than it goes into `longRows`, which is +// scanned in full with the length test intact. A query subtoken of 65+ bytes is a pathology, not a query, +// and this keeps it correct rather than special-casing it out. +struct LexHeadIndex +{ + static constexpr std::size_t kMaxLen = 64; + static constexpr std::size_t kNoRow = ~std::size_t( 0 ); + + strkern::Byteset256 heads; // the 256-bit head set — the SAME byte-set type strkern.h + // already owns, not a second hand-rolled bitmap beside it + std::vector bucketOff; // kMaxLen + 2 entries; CSR offsets by token length + std::vector bucketIdx; + std::vector longRows; + + // The match-table row this corpus token belongs to, or kNoRow. `tokOf( m )` returns row m's + // all-lowercase token; the caller owns the table's storage. + // + // THE PREDICATE IS THE ORIGINAL ONE, character for character — length, then head, then the memcmp + // fast path, then the lexTokenEqualsLowered acronym fallback, in that order. That is what makes the + // whole P2-3 change byte-identical by construction rather than by hope: rows are still visited in + // ascending m, at most one row can match (the table's strings are distinct), and every row this + // skips is one the old linear scan would have rejected on its length or its head alone. + template + std::size_t matchRow( const char* tok, std::size_t tokLen, TokOfFn&& tokOf ) const + { + const unsigned char headByte = ( tok[0] >= 'A' && tok[0] <= 'Z' ) ? static_cast( tok[0] - 'A' + 'a' ) + : static_cast( tok[0] ); + if( !heads.contains( headByte ) ) + { + return kNoRow; // no table row starts with this byte — not one string is touched + } + const char head = char( headByte ); + const std::uint32_t* first = tokLen <= kMaxLen ? bucketIdx.data() + bucketOff[ tokLen ] : longRows.data(); + const std::uint32_t* last = tokLen <= kMaxLen ? bucketIdx.data() + bucketOff[ tokLen + 1 ] : longRows.data() + longRows.size(); + for( ; first != last; ++first ) + { + const std::size_t m = *first; + const std::string& q = tokOf( m ); + if( q.size() == tokLen && q[0] == head + && ( std::memcmp( q.data() + 1, tok + 1, tokLen - 1 ) == 0 || lexTokenEqualsLowered( tok, tokLen, q.data() ) ) ) + { + return m; + } + } + return kNoRow; + } +}; + +// `tokOf( m )` returns row m's (all-lowercase) token. A template rather than a span of strings because the +// caller's match table is an array of structs, and copying its strings out to build an index over them +// would cost more than the index saves. +template +inline LexHeadIndex buildLexHeadIndex( std::size_t rowCount, TokOfFn&& tokOf ) +{ + LexHeadIndex ix; + ix.bucketOff.assign( LexHeadIndex::kMaxLen + 2, 0 ); + for( std::size_t m = 0; m < rowCount; ++m ) + { + const std::string& q = tokOf( m ); + if( q.empty() ) + { + continue; // cannot happen (subtokens() drops < 2 bytes), but q[0] is read below + } + const unsigned char head = static_cast( q[0] ); + ix.heads.add( head ); + if( q.size() <= LexHeadIndex::kMaxLen ) + { + ++ix.bucketOff[ q.size() + 1 ]; // counts, shifted by one: the prefix sum turns them into offsets + } + } + for( std::size_t len = 1; len < ix.bucketOff.size(); ++len ) + { + ix.bucketOff[len] += ix.bucketOff[ len - 1 ]; + } + ix.bucketIdx.resize( ix.bucketOff.back() ); + std::vector fill( ix.bucketOff.begin(), ix.bucketOff.end() ); + for( std::size_t m = 0; m < rowCount; ++m ) + { + const std::string& q = tokOf( m ); + if( q.empty() ) + { + continue; + } + if( q.size() <= LexHeadIndex::kMaxLen ) + { + ix.bucketIdx[ fill[ q.size() ]++ ] = std::uint32_t( m ); // ascending m within each bucket + } + else + { + ix.longRows.push_back( std::uint32_t( m ) ); + } + } + return ix; +} + // THE pass-2 scan text for one file, resolved by ONE rule in ONE place: the docText override when the file // has one, else the file's bytes read into `scratch`. An EMPTY string means "skip this file" — what an empty // docText override and an unreadable file have always meant. `scratch` is the caller's reusable buffer, so @@ -181,14 +299,16 @@ inline const std::string* lexicalScanText( const IngestResult& ing, std::size_t { return &it->second; } + // P2-4 (PLAN_FULL_AUDIT_2026-09-10): this used to be `ifstream` + `ostringstream << rdbuf()` + + // `str()`, which is TWO full copies of every file in the corpus — the stream buffer's growth, then + // `str()`'s copy out of it — on the path that reads every indexed file once per cold query. The + // canonical whole-file read is docparse::detail::readWholeFile (commentcoherence.h, quality.h, + // renamemine.h, githarden.h, graph.h and mergescout.h all already reach for it, and mergescout's own + // comment records that it used to be a hand-rolled copy): one stat, one resize, one fread into the + // caller's buffer, zero intermediate copies. Semantics are identical here by construction — it + // CLEARS `out` on any failure, which is exactly the empty-string "skip this file" contract above. scratch.clear(); - std::ifstream in( diskPath( ing, std::uint32_t( f ) ), std::ios::binary ); - if( in ) - { - std::ostringstream ss; - ss << in.rdbuf(); - scratch = ss.str(); - } + docparse::detail::readWholeFile( diskPath( ing, std::uint32_t( f ) ), scratch ); return &scratch; } @@ -333,6 +453,10 @@ inline std::vector lexicalScoresTiered( const IngestResult& ing, const st } const std::size_t matchCount = matchToks.size(); + // P2-3: build the head mask + length buckets for THIS query's match table (see LexHeadIndex above). + const auto matchTokOf = [ & ]( std::size_t m ) -> const std::string& { return matchToks[m].tok; }; + const LexHeadIndex headIndex = buildLexHeadIndex( matchCount, matchTokOf ); + // per-doc integer stats (SoA): dl[i] = weighted subtoken count, tfFlat[i*matchCount+m] = weighted term // frequency of match-table row m in doc i. Disarmed, matchCount == uniqueCount and the layout is the // historical one byte-for-byte; armed, provisional variant columns sit at m ≥ uniqueCount until the @@ -367,17 +491,10 @@ inline std::vector lexicalScoresTiered( const IngestResult& ing, const st return; } fieldTokenWt += w; - const char* tok = text.data() + tokStartByte; - const char head = ( tok[0] >= 'A' && tok[0] <= 'Z' ) ? char( tok[0] - 'A' + 'a' ) : tok[0]; - for( std::size_t m = 0; m < matchCount; ++m ) + const std::size_t m = headIndex.matchRow( text.data() + tokStartByte, tokLen, matchTokOf ); + if( m != LexHeadIndex::kNoRow ) { - const std::string& q = matchToks[m].tok; - if( q.size() == tokLen && q[0] == head - && ( std::memcmp( q.data() + 1, tok + 1, tokLen - 1 ) == 0 || lexTokenEqualsLowered( tok, tokLen, q.data() ) ) ) - { - tfRow[m] += w; // exact tokens own rows 0..uniqueCount (m == u there) - break; // table strings are distinct → at most one can match - } + tfRow[m] += w; // exact tokens own rows 0..uniqueCount (m == u there) } } ); wtAccum += fieldTokenWt; diff --git a/src/lexindex.h b/src/lexindex.h index 7f781059a..6ca28ff0d 100644 --- a/src/lexindex.h +++ b/src/lexindex.h @@ -14,8 +14,10 @@ #include "model.h" #include "infra/hashutil.h" // fnv1aMultiply — the same sanitizer-clean modulo-2^64 FNV family as the cache hashes +#include "infra/strkern.h" // classMasks — THE byte-parallel character-class kernel the walk below is built on #include +#include #include #include #include @@ -100,67 +102,125 @@ inline bool lexTokenEqualsLowered( const char* tok, std::size_t tokLen, const ch return true; } -// The registered boundary rule, in ONE place because both walkers below need it and a second copy is -// exactly how the acronym bug survived: an UPPERCASE byte at `k` opens a new token when the byte before it -// was not uppercase (the plain camel seam, "fooBar"), or when it is the last upper of an all-caps run that -// a LOWERCASE letter follows (the ACRONYMWord seam, "HTTPServer" -> HTTP|Server). A run followed by end, -// digit or separator stays whole: "MCP" is one token, "MCP2Server" is mcp2|server. The lookahead is one -// byte and the split lands BEFORE the byte that triggers it, so the fused walker's rolling hash never has -// to give a byte back. docs/EVALS.md §4 "Subtoken acronym shredding"; gate: test/subtokencheck.sh. -inline bool lexUpperOpensToken( std::string_view text, std::size_t k, bool prevUpper ) noexcept -{ - const unsigned char next = ( k + 1 < text.size() ) ? static_cast( text[k + 1] ) : 0u; - return !prevUpper || ( next >= 'a' && next <= 'z' ); -} +// ── THE BOUNDARY RULE, AND THE MASK ALGEBRA THAT COMPUTES IT ──────────────────────────────────────── +// +// THE RULE (unchanged since 2026-08-19; docs/EVALS.md §4 "Subtoken acronym shredding"; gate: +// test/subtokencheck.sh). A token is a maximal [A-Za-z0-9] run, cut at two seams: +// * the plain camel seam — an UPPERCASE byte whose predecessor was not uppercase ("fooBar" -> foo|Bar); +// * the ACRONYMWord seam — the LAST uppercase of an all-caps run, and only when a LOWERCASE letter +// follows it ("HTTPServer" -> HTTP|Server). A run followed by end, digit or separator stays whole: +// "MCP" is one token, "MCP2Server" is mcp2|server. +// The lookahead is one byte and the split lands BEFORE the byte that triggers it. Tokens shorter than two +// bytes are dropped BY THE CALLER, exactly as subtokens() does. +// +// Until 2026-09-10 that rule was a per-byte state machine, written out TWICE (once here, once in the +// fused-hash walker below) — the shape that let the acronym bug live in one copy and not the other. It is +// now a single block walk over rw::strkern::classMasks, and the two public walkers are both thin wrappers +// over it, so there is no second copy left to drift. P2-3/S1 of PLAN_FULL_AUDIT_2026-09-10: this walk runs +// over every doc-comment and body field of every symbol, at query time inside the BM25 scan and again at +// index time, so it is one of the few loops in the tree that genuinely touches every byte of the corpus — +// the case where SIMD pays (see the F3 note at the top of infra/strkern.h for the case where it does not). +// +// THE ALGEBRA. With one bit per byte — A = alnum, U = upper, L = lower, `<< 1` meaning "the byte before", +// `>> 1` meaning "the byte after" — the state machine's `emit` points are: +// +// inToken(k) == A[k-1] the old `tokStartByte != kNoTokenByte`: the walker sets a +// start at every alnum byte and clears it at every +// separator, so "in a token at k" is exactly "byte k-1 was +// alnum". prevUpper likewise IS U[k-1] whenever A[k-1] holds +// (the walker's reset-to-false at a separator only matters +// when A[k-1] is false, and A[k-1] already gates the term). +// split = U & (A<<1) & ( ~(U<<1) | (L>>1) ) an upper byte, inside a token, whose +// predecessor was not upper (camel) OR whose +// successor is lower (ACRONYMWord) +// starts = ( A & ~(A<<1) ) | split a run's first byte, plus every split point +// cuts = starts | ~A where a token can END: at the next start, or +// at the next separator +// +// A token therefore runs from each `starts` bit to the NEXT `cuts` bit (or to the end of the text). Worked +// through by hand on the seam table, which is also how test/strkern_harness.cpp's arm F1 pins it: +// "fooBar" A=111111 U=bit3 split={3} starts={0,3} -> foo | Bar +// "HTTPServer" U={0..4} L={5..9} split={4} starts={0,4} -> HTTP | Server +// (k=1..3 fail: predecessor IS upper and successor is not lower) +// "MCP2Server" U={0,1,2,4} digit={3} split={4} starts={0,4} -> MCP2 | Server +// (k=4 passes on the CAMEL half: U[3] is false, byte 3 being a digit) +// "MCP" no k has a lower successor split={} starts={0} -> MCP +// +// Correctness is not argued from this comment: test/strkerncheck.sh's arms F1/F2/G4 compare these walkers +// against VERBATIM copies of the pre-2026-09-10 state machines on 100k random buffers (including a +// camel/acronym-dense alphabet), on the seam table above, and on every byte of src/ and docs/ — spans AND +// fused hashes, byte for byte. -// The ONE subtoken state machine (extracted verbatim from lexical.h scanField so index-time and query-time -// tokenization are the same function): a token is a maximal alphanumeric run between separators, cut at a -// lower/digit → Upper transition and at the LAST uppercase of an all-caps run of ≥2 that a lowercase -// letter follows (the ACRONYMWord rule — "HTTPServer" → HTTP|Server). Emits RAW [tokStartByte, tokEndByte) -// spans; callers apply the ≥2-byte drop themselves (mirroring subtokens()/scanField exactly). +// The ONE subtoken state machine. Emits RAW [tokStartByte, tokEndByte) spans; callers apply the >= 2-byte +// drop themselves (mirroring subtokens()/scanField exactly). // -// 2026-08-19: before this date the rule read "an interior uppercase char always starts a NEW token", which -// made every all-caps run a string of 1-byte tokens that the ≥2-byte drop then discarded — an acronym was -// indexed as nothing at all. A token's non-first bytes can now be UPPERCASE, so anything downstream that -// used to exploit "only the FIRST byte can be uppercase" must normalize the whole token: lexSubtokenHash -// and the fused walker below do, and so does lexical.h's scanTextInto matcher. Registered + measured in -// docs/EVALS.md §4 "Subtoken acronym shredding"; gate: test/subtokencheck.sh (arms B and C pin exactly -// this mirror against subtokens() and against lexSubtokenHash()). -template -inline void forEachLexSubtoken( std::string_view text, EmitFn&& emit ) +// Block-at-a-time, kBlockBytes at a time (16 on NEON, 32 on AVX2), with three carries across the block +// seam: whether the byte before the block was alnum and whether it was upper (the `<< 1` terms), and +// whether the byte AFTER the block is lowercase (the `>> 1` term — read as a single byte, since it is one +// byte and reading it here is cheaper than keeping a lookahead register). `pending` carries a token that +// began in an earlier block, so a token straddling any number of blocks is emitted once, with its true +// start and end. +template +inline void forEachLexTokenSpan( std::string_view text, EmitSpanFn&& emitSpan ) { constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); - std::size_t tokStartByte = kNoTokenByte; - bool prevUpper = false; - for( std::size_t k = 0; k < text.size(); ++k ) + const std::size_t n = text.size(); + const char* p = text.data(); + std::size_t pending = kNoTokenByte; // start byte of a token still looking for its end + bool prevAlnum = false; // A[base-1] + bool prevUpper = false; // U[base-1] + + for( std::size_t base = 0; base < n; base += strkern::kBlockBytes ) { - const unsigned char c = static_cast( text[k] ); - const bool upper = c >= 'A' && c <= 'Z'; - const bool lower = c >= 'a' && c <= 'z'; - const bool digit = c >= '0' && c <= '9'; - if( !upper && !lower && !digit ) // separator - { - if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k ); tokStartByte = kNoTokenByte; } - prevUpper = false; - continue; - } - if( upper && tokStartByte != kNoTokenByte && lexUpperOpensToken( text, k, prevUpper ) ) // camel / ACRONYMWord boundary - { - emit( tokStartByte, k ); - tokStartByte = k; - } - if( tokStartByte == kNoTokenByte ) + const std::size_t width = ( n - base < strkern::kBlockBytes ) ? ( n - base ) : strkern::kBlockBytes; + strkern::Masks m; + strkern::classMasks( p + base, width, m ); + + // `1u << 32` is undefined, and width IS 32 on the AVX2 block — hence the explicit all-ones case + // rather than a shift that happens to work on this compiler. + const std::uint32_t valid = ( width >= 32 ) ? ~std::uint32_t( 0 ) : ( ( std::uint32_t( 1 ) << width ) - 1u ); + + const unsigned char after = ( base + width < n ) ? static_cast( p[ base + width ] ) : 0u; + const std::uint32_t nextLowerBit = ( after >= 'a' && after <= 'z' ) ? ( std::uint32_t( 1 ) << ( width - 1 ) ) : 0u; + + const std::uint32_t alnumShift = ( m.alnum << 1 ) | ( prevAlnum ? 1u : 0u ); // bit k = A[k-1] + const std::uint32_t upperShift = ( m.upper << 1 ) | ( prevUpper ? 1u : 0u ); // bit k = U[k-1] + const std::uint32_t lowerAhead = ( m.lower >> 1 ) | nextLowerBit; // bit k = L[k+1] + + const std::uint32_t split = m.upper & alnumShift & ( ~upperShift | lowerAhead ) & valid; + const std::uint32_t starts = ( m.alnum & ~alnumShift & valid ) | split; + std::uint32_t cuts = starts | ( ~m.alnum & valid ); + + while( cuts != 0 ) { - tokStartByte = k; + const unsigned bitIndex = unsigned( std::countr_zero( cuts ) ); + cuts &= cuts - 1u; + const std::size_t pos = base + bitIndex; + if( pending != kNoTokenByte ) + { + emitSpan( pending, pos ); + } + pending = ( ( starts >> bitIndex ) & 1u ) != 0 ? pos : kNoTokenByte; } - prevUpper = upper; + + const std::uint32_t lastBit = std::uint32_t( 1 ) << ( width - 1 ); + prevAlnum = ( m.alnum & lastBit ) != 0; + prevUpper = ( m.upper & lastBit ) != 0; } - if( tokStartByte != kNoTokenByte ) + + if( pending != kNoTokenByte ) { - emit( tokStartByte, text.size() ); + emitSpan( pending, n ); // a token that runs to the end of the text has no cut byte to end it } } +// The query-time walker: spans only (scanField string-compares instead of hashing). +template +inline void forEachLexSubtoken( std::string_view text, EmitFn&& emit ) +{ + forEachLexTokenSpan( text, [ & ]( std::size_t tokStartByte, std::size_t tokEndByte ) { emit( tokStartByte, tokEndByte ); } ); +} + // FNV-1a 64 over the token's NORMALIZED bytes (EVERY byte lowercased) — so hashing a corpus token equals // hashing the all-lowercase query subtoken it would string-match. Lowercasing only the first byte was // enough until 2026-08-19, when the state machine stopped shredding all-caps runs: a token may now be @@ -187,58 +247,37 @@ struct RawDefLex std::vector tokenTfs; // weighted term frequency per hash (exact integers) }; -// ── B0 round 2: the fused-hash tokenizer walk — forEachLexSubtoken with the FNV-1a rolling INSIDE the -// state machine, so the index-time stats builder touches each byte ONCE (the split shape walked every -// token's bytes twice: once to find the span, once to hash it — this seam is the rich-parse tail the -// index/cold budgets pay). Emits ( tokStartByte, tokEndByte, normalizedHash ); the hash is EXACTLY -// lexSubtokenHash( text + tokStartByte, len ): EVERY byte is lowercased before mixing, so no other -// normalization exists to drift. (Lowercasing only the first byte was equivalent until 2026-08-19, when -// an all-caps run stopped being shredded and interior uppercase became reachable — see lexSubtokenHash.) -// The boundary rule is the walker's above, one char of lookahead and no retroactive un-mixing: a split -// happens BEFORE the byte that triggers it, so the running hash never has to give a byte back. -// Query-time scanField keeps the hash-free walker above (it string-compares instead). +// ── the index-time walker: the same spans, plus each token's normalized hash ───────────────────────── +// Emits ( tokStartByte, tokEndByte, normalizedHash ), where the hash is EXACTLY +// lexSubtokenHash( text + tokStartByte, len ). It used to be a SECOND copy of the state machine with the +// FNV rolling inside it — the "touch each byte once" shape from B0 round 2. That shape is gone because +// the classification is no longer per-byte work at all: the block walk classifies 16 (NEON) or 32 (AVX2) +// bytes at a time, and the hash then runs over the token's bytes only. Two consequences worth stating: +// the state machine now exists ONCE (the drift risk B0's own header warns about is structurally gone), +// and the bytes the hash re-reads are the ~60-70% of the corpus that are inside tokens, at L1 distance, +// having just been touched by the classifier. +// +// THE FOLD IS BRANCHLESS AND EXACT (audit item S2, Lemire's SWAR case-fold identity): every byte of a +// token is [A-Za-z0-9] by construction, and for exactly that set `c | ( ( c & 0x40 ) >> 1 )` equals +// lexLowerByte( c ) — 'A' (0x41) has the 0x40 bit and gains 0x20; 'a' (0x61) has it and already carries +// 0x20, so the OR is a no-op; a digit (0x30..0x39) has no 0x40 bit and is left alone. It is NOT a general +// ASCII fold and must never be used on a byte that has not already been classified as alnum: '@' (0x40) +// would become '`'. lexSubtokenHash below keeps the general, range-tested form for external callers, and +// test/strkerncheck.sh arm F2 asserts the two agree on every token of every file in src/ and docs/. template inline void forEachLexSubtokenHashed( std::string_view text, EmitFn&& emit ) { - constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); - constexpr std::uint64_t kFnvBasis = 1469598103934665603ull; - std::size_t tokStartByte = kNoTokenByte; - std::uint64_t h = kFnvBasis; - bool prevUpper = false; - const auto mix = [ & ]( unsigned char c ) noexcept { h = hashutil::fnv1aAbsorb( h, char( lexLowerByte( c ) ) ); }; - const auto beginToken = [ & ]( unsigned char c, std::size_t k ) noexcept + constexpr std::uint64_t kFnvBasis = 1469598103934665603ull; + forEachLexTokenSpan( text, [ & ]( std::size_t tokStartByte, std::size_t tokEndByte ) { - tokStartByte = k; - h = kFnvBasis; - mix( c ); - }; - for( std::size_t k = 0; k < text.size(); ++k ) - { - const unsigned char c = static_cast( text[k] ); - const bool upper = c >= 'A' && c <= 'Z'; - const bool lower = c >= 'a' && c <= 'z'; - const bool digit = c >= '0' && c <= '9'; - if( !upper && !lower && !digit ) // separator + std::uint64_t h = kFnvBasis; + for( std::size_t k = tokStartByte; k < tokEndByte; ++k ) { - if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k, h ); tokStartByte = kNoTokenByte; } - prevUpper = false; - continue; + const unsigned char c = static_cast( text[k] ); + h = hashutil::fnv1aAbsorb( h, char( c | ( ( c & 0x40u ) >> 1 ) ) ); } - if( upper && tokStartByte != kNoTokenByte && lexUpperOpensToken( text, k, prevUpper ) ) // camel / ACRONYMWord boundary - { - emit( tokStartByte, k, h ); - beginToken( c, k ); - prevUpper = true; - continue; - } - if( tokStartByte == kNoTokenByte ) { beginToken( c, k ); prevUpper = upper; continue; } - mix( c ); // interior byte (lowercased: a run's tail is uppercase) - prevUpper = upper; - } - if( tokStartByte != kNoTokenByte ) - { - emit( tokStartByte, text.size(), h ); - } + emit( tokStartByte, tokEndByte, h ); + } ); } // Build one def's stats from the SAME spans lexical.h Pass 2 scans: doc-comment [docCommentStart, bodyStart) diff --git a/test/strkern_harness.cpp b/test/strkern_harness.cpp index d2ab779e2..1bcf932a2 100644 --- a/test/strkern_harness.cpp +++ b/test/strkern_harness.cpp @@ -206,6 +206,15 @@ static void armAllBytes() // Kept byte-for-byte as they stood at 05f4b892 (src/lexindex.h:130 and :201) so this arm compares the new // mask-driven walkers against the OLD code, not against a paraphrase of it. Do not "clean these up". +// lexUpperOpensToken went with them: it was the state machines' one-byte lookahead, and the mask algebra +// that replaced them states the same rule as `U & (A<<1) & ( ~(U<<1) | (L>>1) )`. Kept HERE, verbatim, +// because a reference walker that borrowed the shipped rule would move with it and prove nothing. +static bool refLexUpperOpensToken( std::string_view text, std::size_t k, bool prevUpper ) noexcept +{ + const unsigned char next = ( k + 1 < text.size() ) ? static_cast< unsigned char >( text[ k + 1 ] ) : 0u; + return !prevUpper || ( next >= 'a' && next <= 'z' ); +} + template< class EmitFn > static void refForEachLexSubtoken( std::string_view text, EmitFn&& emit ) { @@ -224,7 +233,7 @@ static void refForEachLexSubtoken( std::string_view text, EmitFn&& emit ) prevUpper = false; continue; } - if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + if( upper && tokStartByte != kNoTokenByte && refLexUpperOpensToken( text, k, prevUpper ) ) { emit( tokStartByte, k ); tokStartByte = k; @@ -268,7 +277,7 @@ static void refForEachLexSubtokenHashed( std::string_view text, EmitFn&& emit ) prevUpper = false; continue; } - if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + if( upper && tokStartByte != kNoTokenByte && refLexUpperOpensToken( text, k, prevUpper ) ) { emit( tokStartByte, k, h ); beginToken( c, k ); @@ -292,45 +301,32 @@ struct Tok std::uint64_t hash = 0; }; -static void collectRef( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - refForEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) - { - out.push_back( { s, e, h } ); - } ); -} - -static void collectNew( std::string_view text, std::vector< Tok >& out ) +// ONE collector, driving whichever walker it is handed — deliberately not four (or two) near-identical +// wrappers, which is a clone group the repo's own --quality-delta would (and did) flag. The default +// argument on the sink lets the SAME lambda serve the two-argument span walker and the three-argument +// hashed one. +template< class Walk > +static void collect( std::string_view text, std::vector< Tok >& out, Walk&& walk ) { out.clear(); - rw::forEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) - { - out.push_back( { s, e, h } ); - } ); -} - -// spans only (the hash-free walker) — a separate list, because the two shipped walkers are separate code -static void collectRefSpans( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - refForEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); + walk( text, [ &out ]( std::size_t s, std::size_t e, std::uint64_t h = 0 ) { out.push_back( { s, e, h } ); } ); } -static void collectNewSpans( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - rw::forEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); -} +// the four drivers, as the thinnest possible adapters over the function templates (which cannot be +// passed as values) +inline constexpr auto kRefHashed = []( std::string_view t, auto&& f ) { refForEachLexSubtokenHashed( t, f ); }; +inline constexpr auto kNewHashed = []( std::string_view t, auto&& f ) { rw::forEachLexSubtokenHashed( t, f ); }; +inline constexpr auto kRefSpans = []( std::string_view t, auto&& f ) { refForEachLexSubtoken( t, f ); }; +inline constexpr auto kNewSpans = []( std::string_view t, auto&& f ) { rw::forEachLexSubtoken( t, f ); }; // Compare all four lists for one text. Returns "" when identical, else the first divergence. static std::string tokenizerDiff( std::string_view text ) { static std::vector< Tok > refH, newH, refS, newS; - collectRef( text, refH ); - collectNew( text, newH ); - collectRefSpans( text, refS ); - collectNewSpans( text, newS ); + collect( text, refH, kRefHashed ); + collect( text, newH, kNewHashed ); + collect( text, refS, kRefSpans ); + collect( text, newS, kNewSpans ); char msg[ 384 ]; if( refS.size() != newS.size() ) From eb7a1dc0041aaba068b22713a66b60eff547e61f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:30:30 -0400 Subject: [PATCH 13/73] test(cache): gate the eviction pin and the warm-path reserve BEFORE the code MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gates for the 2026-09-10 full audit's P1-1 (highest) and P1-11 (high), written first and proved red against the pre-change binary. test/evictioncheck.sh gains three arms for P1-1 — the dir-wide 2 GB oldest-first sweep evicts the SIBLING FAMILY OF THE ROOT THE USER IS WORKING IN: (h) two roots, the MRU root's sibling family seeded as the OLDEST blob in the dir: it must survive and the other root's blob must be what goes, with one `ripwire: cache …` line on stderr. (i) the pinned set ALONE over the budget: kept anyway, said once on stderr. (j) a sweep that evicts nothing: ZERO stderr bytes (the disclosure is conditional, so no ordinary run and no stderr-comparing gate grows a line). The arms exercise the REAL 2 GB constant with sparse `truncate -s` fillers, so no test-only override env var is introduced and the number under test is the shipped one. Red against 05f4b892's binary: FAIL (h) the MRU root's sibling family was EVICTED FAIL (h) the other root's blob survived FAIL (h) an eviction happened with ZERO disclosure FAIL (i) the pinned set was evicted when nothing else could be freed FAIL (i) the pinned set exceeded the budget with no disclosure ... (j) and every pre-existing arm PASS on that same binary test/cachereservecheck.sh is new: `warm_growths=` on the RIPWIRE_CACHE_STATS line must be 0 on a single-threaded warm run (one-file Ruby and JS fixtures, so each family's per-thread reserve IS its exact total), the warm map must be byte-identical to the --no-cache map, and the observable must stay OFF by default. Red twice: the pre-change binary emits no `warm_growths=` field at all, and a mutation that deletes only the four added family reserves reports warm_growths=1 on both fixtures. Listed in test/regression.sh in this commit; docs/gatecount_build.py regenerated (586 -> 587) rather than hand-edited. Co-Authored-By: Claude Fable 5.1 --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- test/cachereservecheck.sh | 158 +++++++++++++++++++++++++++++++++ test/evictioncheck.sh | 143 ++++++++++++++++++++++++++++- test/regression.sh | 2 +- 6 files changed, 309 insertions(+), 10 deletions(-) create mode 100755 test/cachereservecheck.sh diff --git a/README.md b/README.md index dde8da76a..d157ea9f3 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-586 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +587 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 **586 gate scripts** and is the authoritative list; +`test/regression.sh` names **587 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 95b74417f..1b1058118 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` | 586 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 587 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) @@ -5625,7 +5625,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 **586 gate scripts**, all of which exist on disk. +naming **587 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`. @@ -6637,7 +6637,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 586. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 587. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 0647c42fe..fc5a0220f 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["586 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 + ["587 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "586", "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, "587", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["586 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/test/cachereservecheck.sh b/test/cachereservecheck.sh new file mode 100755 index 000000000..2d2207401 --- /dev/null +++ b/test/cachereservecheck.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# cachereservecheck.sh — gate for P1-11 (2026-09-10 full audit): THE WARM CACHE PATH MUST NOT REALLOCATE +# ITS ACCUMULATORS, and the claim must be executable rather than read off a profile. +# +# WHAT THE AUDIT SAW. Leaf-of-stack attribution on a WARM llvm `--grep` (8,484 busy samples) put +# `std::vector::push_back` at 12.55%, `RawDef` at 5.14%, `RawBind` at 4.00% and the `_platform_memmove` +# those reallocations force at 8.38% — ~30% of the run — and attributed them to `loadCache`'s deserialize +# plus `rw::ingest`'s merge, concluding the fix was "reserve() before the loops". +# +# WHAT READING THE CODE FOUND. Two of the three suspects already reserve EXACTLY: +# - `readFileRecord` (ingest_cache.h) reserves every one of a file's eight families from the record's own +# count before its loop, and has since the count-validation work; +# - `mergeThreadFacts` (ingest_parsepool.h) reserves each family's exact cross-thread total. +# The one accumulator that could still grow is the PER-THREAD WARM-HIT accumulator that `appendCacheHitFacts` +# feeds — and it reserved only FOUR of the eight families (defs/refs/incs/binds). ffis, routeDefs, routeUses +# and constOpens doubled up from zero on every warm run. The cold path skips those four deliberately +# (`coldParseReserve` only has a bytes-based ESTIMATE and says so), but the warm path is not estimating: the +# cached FileFacts carry the EXACT counts, so summing them is one more add in a loop that already runs and +# `reserve( 0 )` costs nothing when a family is empty. +# +# THE OBSERVABLE. `warm_growths=` on the RIPWIRE_CACHE_STATS line counts, once per family per file, an append +# that was about to cross capacity. Zero is the contract on a single-threaded warm run: with nfiles == 1 the +# pool runs one thread, so each family's per-thread reserve IS its exact total and nothing may reallocate. +# (A multi-threaded run can still show a small non-zero count — that is the lock-free work queue's own skew, +# a worker drawing more than its 1/nthreads share, not a missing reserve. Measured on golang/go, 11,003 files +# warm on 18 threads: 29-36. The gate does not assert on that number; the commit message records it.) +# +# Checks: +# (A) a one-file RUBY fixture (module/class opens → constOpens; `require` → incs) reports warm_growths=0. +# (B) a one-file JS fixture (imports + express routes + fetch → routeDefs/routeUses) reports warm_growths=0. +# (C) a multi-file mixed fixture: the WARM map is byte-identical to the --no-cache map (a reserve must never +# change an answer), and the stats line still carries the observable on the real multi-thread path. +# (D) the observable is OFF by default — a warm run without RIPWIRE_CACHE_STATS writes ZERO stderr bytes. +# (E) the warm map is deterministic (two runs cmp equal) and well-formed. +# +# RED-FIRST. Arms (A) and (B) fail against any binary that does not reserve all eight warm families: the +# pre-change binary emits no `warm_growths=` field at all (the assertion cannot find its zero), and a +# mutation that deletes just the four added reserves reports warm_growths=1 on (A) and (B). +# +# Usage: test/cachereservecheck.sh | RIPWIRE_BIN=build/ripwire test/cachereservecheck.sh +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ echo " PASS $1"; } +no(){ echo " FAIL $1"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +echo "cachereservecheck: BIN=$BIN" + +# Each fixture gets its own private TMPDIR so the auto-cache under test is one blob we own end to end +# (and so a shared cache dir's eviction sweep can never perturb an arm). +warmgrowths(){ # $1 = repo dir, $2 = private cache base — prime, then re-run warm and echo warm_growths=N + env -u XDG_CACHE_HOME TMPDIR="$2" "$BIN" "$1" >/dev/null 2>&1 + env -u XDG_CACHE_HOME TMPDIR="$2" RIPWIRE_CACHE_STATS=1 "$BIN" "$1" 2>&1 >/dev/null \ + | sed -n 's/.*\(warm_growths=[0-9]*\).*/\1/p' | head -1 +} + +# ── (A) one Ruby file: constOpens (module/class opens) + incs (require) ───────────────────────────── +A_REPO="$TMP/a/repo"; A_CB="$TMP/a/cb"; mkdir -p "$A_REPO" "$A_CB/ripwire" +cat > "$A_REPO/widget.rb" <<'RB' +require 'json' + +module Outer + class Widget + def render( x ) + JSON.generate( x ) + end + + def to_s + render( { name: 'w' } ) + end + end +end +RB +ga="$( warmgrowths "$A_REPO" "$A_CB" )" +[ "$ga" = "warm_growths=0" ] && ok "(A) one-file Ruby fixture: $ga (constOpens/incs reserved exactly)" \ + || no "(A) one-file Ruby fixture reported '${ga:-}' — expected warm_growths=0" + +# ── (B) one JS file: imports, express route registrations, a client fetch ─────────────────────────── +B_REPO="$TMP/b/repo"; B_CB="$TMP/b/cb"; mkdir -p "$B_REPO" "$B_CB/ripwire" +cat > "$B_REPO/server.js" <<'JS' +import express from 'express'; +import { helper } from './helper.js'; + +const app = express(); + +app.get( '/widgets/:id', function getWidget( req, res ) { res.send( helper( req.params.id ) ); } ); +app.post( '/widgets', function addWidget( req, res ) { res.send( 'ok' ); } ); + +async function pullWidget( id ) { + return await fetch( '/api/widgets/' + id ); +} +JS +gb="$( warmgrowths "$B_REPO" "$B_CB" )" +[ "$gb" = "warm_growths=0" ] && ok "(B) one-file JS fixture: $gb (routeDefs/routeUses/ffis reserved exactly)" \ + || no "(B) one-file JS fixture reported '${gb:-}' — expected warm_growths=0" + +# ── (C) multi-file, multi-language: a reserve must never change an answer ─────────────────────────── +C_REPO="$TMP/c/repo"; C_CB="$TMP/c/cb"; mkdir -p "$C_REPO" "$C_CB/ripwire" +cp "$A_REPO/widget.rb" "$C_REPO/" +cp "$B_REPO/server.js" "$C_REPO/" +cat > "$C_REPO/core.cpp" <<'CPP' +#include + +namespace core +{ +int widen( int x ) +{ + return x * 2; +} + +std::string label( int x ) +{ + return std::to_string( widen( x ) ); +} +} +CPP +cat > "$C_REPO/svc.py" <<'PY' +import json + + +def encode(payload): + return json.dumps(payload) + + +def decode(blob): + return json.loads(blob) +PY + +env -u XDG_CACHE_HOME TMPDIR="$C_CB" "$BIN" "$C_REPO" --top-k=100000 >"$TMP/c_prime.xml" 2>/dev/null +env -u XDG_CACHE_HOME TMPDIR="$C_CB" "$BIN" "$C_REPO" --top-k=100000 >"$TMP/c_warm.xml" 2>"$TMP/c_warm.err" +"$BIN" "$C_REPO" --top-k=100000 --no-cache >"$TMP/c_cold.xml" 2>/dev/null +cmp -s "$TMP/c_warm.xml" "$TMP/c_cold.xml" \ + && ok "(C) the WARM map is byte-identical to the --no-cache map (the reserve changes no answer)" \ + || no "(C) warm and cold maps differ — the warm accumulator path changed an answer" + +gc="$( env -u XDG_CACHE_HOME TMPDIR="$C_CB" RIPWIRE_CACHE_STATS=1 "$BIN" "$C_REPO" 2>&1 >/dev/null \ + | sed -n 's/.*\(warm_growths=[0-9]*\).*/\1/p' | head -1 )" +[ -n "$gc" ] && ok "(C) the observable survives the real multi-file path: $gc" \ + || no "(C) no warm_growths field on a multi-file warm run" + +# ── (D) the observable is OFF by default ─────────────────────────────────────────────────────────── +[ ! -s "$TMP/c_warm.err" ] && ok "(D) a warm run without RIPWIRE_CACHE_STATS writes ZERO stderr bytes" \ + || { no "(D) the warm run wrote to stderr with the stats env var unset"; cat "$TMP/c_warm.err"; } + +# ── (E) determinism + well-formedness of the warm map ────────────────────────────────────────────── +env -u XDG_CACHE_HOME TMPDIR="$C_CB" "$BIN" "$C_REPO" --top-k=100000 >"$TMP/c_warm2.xml" 2>/dev/null +cmp -s "$TMP/c_warm.xml" "$TMP/c_warm2.xml" && ok "(E) two warm runs are byte-identical (determinism)" \ + || no "(E) two warm runs differ — the warm path is not deterministic" +if command -v xmllint >/dev/null 2>&1; then + xmllint --noout "$TMP/c_warm.xml" 2>/dev/null && ok "(E) warm map is well-formed XML" || no "(E) warm map is malformed XML" +fi + +[ "$fail" -eq 0 ] && echo "cachereservecheck: ALL PASS" || { echo "cachereservecheck: SOME CHECKS FAILED"; exit 1; } diff --git a/test/evictioncheck.sh b/test/evictioncheck.sh index 41691dc0b..878351ade 100755 --- a/test/evictioncheck.sh +++ b/test/evictioncheck.sh @@ -33,8 +33,14 @@ # saveCache publishes via tmp-then-rename, and a double fs::remove of an already-gone file is a # benign ENOENT no-op — so two sweepers racing on the same stale blob is safe by construction. # +# (h) P1-1: the MRU root's SIBLING family is PINNED through a byte-budget sweep — the sweep takes another +# root's blob instead, and says so once on stderr. +# (i) P1-1: when the pinned set ALONE exceeds the budget it is kept anyway, said once on stderr. +# (j) P1-1: a sweep that evicts nothing writes ZERO bytes to stderr (the disclosure is conditional). +# # Sparse filler (truncate -s) keeps the ">2 GB" file logically oversized (what fs::file_size measures) -# without touching real disk, so the gate stays fast. Does NOT edit regression.sh. +# without touching real disk, so the gate stays fast — which is also why arms (h)-(j) can exercise the REAL +# 2 GB budget rather than a test-only override. Does NOT edit regression.sh. # Usage: test/evictioncheck.sh | RIPWIRE_BIN=build_r2a1/ripwire test/evictioncheck.sh set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" @@ -240,4 +246,139 @@ kill "$HOLDER" 2>/dev/null; wait "$HOLDER" 2>/dev/null # (d) .bin behavior unchanged: already covered above by the pre-existing OLD/FILLER/FRESH .bin arms (both # flat and sharded layouts), which this section's separate TMPDIR/CACHEDIR does not touch or interact with. +# ── (h)(i)(j) P1-1 (2026-09-10 audit) — the MRU ROOT'S OWN FAMILIES ARE PINNED DURING A BUDGET SWEEP ──── +# THE DEFECT. One llvm-project root needs 1.76 GB of cache for its OWN two families (rich 1.19 GB + lean +# 0.57 GB) against a dir-wide 2 GB oldest-first sweep. Add anything else — a second corpus, or one +# --edit-check HEAD snapshot (0.52 GB on llvm) — and the sweep evicts the SIBLING FAMILY OF THE SAME ROOT, +# because evictOldCacheFamily only ever protected `keepPath` (the one blob being written). Measured, same +# argv, same session, same binary: `--grep=SmallVector` 20 s → 206 s, `--for=...` 19 s → 268 s, and the +# ping-pong is self-sustaining (each cold run's save evicts the other family again). Zero disclosure: all +# four .err files were 0 bytes. The 2 GB constant is a blow-up guard and is NOT lowered (owner rule +# `quality-first-caps-are-blowup-guards`); the eviction ORDER is what changes. +# +# THE CONTRACT UNDER TEST (quality.h: cacheBlobRootKey + evictOldCacheFamily's size pass): +# (h) every blob of the MRU root — lean, rich, qheadsnap, qsnap, … — is PINNED for the duration of a +# byte-budget sweep; the sweep takes OTHER roots first, oldest-first, exactly as before. Red-first: +# against the pre-change binary the pinned sibling is the FIRST thing deleted (it is the oldest). +# (i) if the pinned set ALONE still exceeds the budget, it is kept anyway (evicting it would force the +# full re-parse this whole change exists to prevent) and ONE `ripwire: cache …` line says so. That +# line is a plain stderr emit, never DEGRADED_PATH_ALERT: NDEBUG compiles the alert out and the +# whole point is that a Release binary discloses this too. +# (j) the disclosure is CONDITIONAL: a run whose sweep evicts nothing writes ZERO bytes to stderr. +# The PIN KEY is the 16-hex fnv1a64(realpath(root)) field that every family's filename already carries — +# defaultCachePath's `ripwire--{lean,rich}.bin` and shaKeyedCachePath's +# `ripwire----.bin` alike (headSnapRepoHex hashes the same material as +# defaultCachePath), so no plumbing is needed: the sweep reads it off `keepPath` itself. +# +# The AGE pass is deliberately NOT pinned — a blob nobody has touched in 30 days is stale by the hygiene +# policy's own definition, and its eviction costs one cold parse rather than a self-sustaining ping-pong. +# Every arm below therefore seeds mtimes inside the 30-day window, so only the size pass can fire. +# +# Sparse fillers again (truncate -s), so these arms exercise the REAL 2 GB budget — no test-only override +# env var is introduced, and the constant under test is the shipped one. + +# apparent bytes of every ripwire-*.bin under an arbitrary cache dir (the arms below each own a private +# one, so the file-scope allblobs()/dirapparentbytes() — which are bound to $CACHEDIR — do not apply). +dirbytesof(){ + local total=0 f sz + while IFS= read -r f; do + [ -e "$f" ] || continue + sz="$( apparentsize "$f" )" + total=$(( total + sz )) + done < <( find "$1" -mindepth 1 -maxdepth 2 -name 'ripwire-*.bin' 2>/dev/null ) + echo "$total" +} + +# ---- (h) two roots, the MRU root's sibling family is the OLDEST blob in the dir -------------------- +TMP3="$( mktemp -d )"; trap 'rm -rf "$TMP" "$TMP2" "$TMP3"' EXIT +CB3="$TMP3/cachebase"; CD3="$CB3/ripwire"; mkdir -p "$CD3" +R3="$TMP3/repo"; mkdir -p "$R3" +printf 'int pinme( void )\n{\n return 1;\n}\n' > "$R3/f.cpp" + +env -u XDG_CACHE_HOME TMPDIR="$CB3" "$BIN" "$R3" >/dev/null 2>"$TMP3/prime.err" +OWN3="$( find "$CD3" -mindepth 1 -maxdepth 2 -name 'ripwire-*.bin' 2>/dev/null | head -1 )" +ROOTHEX3="$( basename "${OWN3:-none}" | sed -E 's/^ripwire-([0-9a-f]{16})-lean\.bin$/\1/' )" +if printf '%s' "$ROOTHEX3" | grep -qE '^[0-9a-f]{16}$'; then + ok "(h) primed: this root's lean blob names root key $ROOTHEX3" +else + no "(h) could not read a 16-hex root key off the primed blob (own='$OWN3')" +fi + +# the SIBLING family of the SAME root — seeded FLAT (the sweep must find it in either layout) and made +# the OLDEST blob in the dir, which is exactly what the pre-change oldest-first sweep deletes first. +SIB3="$CD3/ripwire-$ROOTHEX3-rich.bin" +truncate -s 1200M "$SIB3" +sleep 1 +# a DIFFERENT root's blob, newer and bigger — the one an oldest-first sweep would keep, and the one the +# fixed sweep must take instead. +OTHER3="$CD3/ripwire-00000000deadf00d-lean.bin" +truncate -s 1500M "$OTHER3" + +b3="$( dirbytesof "$CD3" )" +[ "$b3" -gt 2147483648 ] && ok "(h) seed: dir exceeds the 2 GB budget (~$b3 bytes: 1200M sibling + 1500M other root)" \ + || no "(h) seed: dir does not exceed budget (~$b3 bytes) — fillers too small" + +printf 'int pinme2( void )\n{\n return 2;\n}\n' >> "$R3/f.cpp" # Win-2: saveCache (and the sweep) only run when something changed +env -u XDG_CACHE_HOME TMPDIR="$CB3" "$BIN" "$R3" >"$TMP3/run.xml" 2>"$TMP3/run.err" +rc4=$? +[ "$rc4" -eq 0 ] && ok "(h) run exits 0" || { no "(h) run exited $rc4"; cat "$TMP3/run.err"; } +grep -q 'n="pinme"' "$TMP3/run.xml" 2>/dev/null && ok "(h) run output still correct (pinme present)" || no "(h) run output missing pinme()" + +[ -e "$SIB3" ] && ok "(h) the MRU root's SIBLING family survives a budget sweep (pinned) — P1-1's 206 s ping-pong" \ + || no "(h) the MRU root's sibling family was EVICTED — the sweep still takes the blob this root is about to need" +[ ! -e "$OTHER3" ] && ok "(h) the OTHER root's blob is what the sweep took instead" \ + || no "(h) the other root's blob survived — the sweep did not free the bytes it needed" + +[ -s "$TMP3/run.err" ] && ok "(h) the eviction is DISCLOSED on stderr (was 0 bytes before this change)" \ + || no "(h) an eviction happened with ZERO disclosure — the honesty rule does not reach the cache layer" +grep -q '^ripwire: cache ' "$TMP3/run.err" 2>/dev/null && ok "(h) the disclosure uses the house 'ripwire: cache …' shape" \ + || { no "(h) no 'ripwire: cache …' line on stderr"; cat "$TMP3/run.err"; } +errlines3="$( wc -l < "$TMP3/run.err" | tr -d ' ' )" +[ "$errlines3" -eq 1 ] && ok "(h) exactly ONE disclosure line (not one per evicted blob)" \ + || no "(h) expected 1 stderr line, got $errlines3" + +# ---- (i) the pinned set ALONE exceeds the budget → kept anyway, said once --------------------------- +TMP4="$( mktemp -d )"; trap 'rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4"' EXIT +CB4="$TMP4/cachebase"; CD4="$CB4/ripwire"; mkdir -p "$CD4" +R4="$TMP4/repo"; mkdir -p "$R4" +printf 'int solo( void )\n{\n return 1;\n}\n' > "$R4/f.cpp" + +env -u XDG_CACHE_HOME TMPDIR="$CB4" "$BIN" "$R4" >/dev/null 2>/dev/null +OWN4="$( find "$CD4" -mindepth 1 -maxdepth 2 -name 'ripwire-*.bin' 2>/dev/null | head -1 )" +ROOTHEX4="$( basename "${OWN4:-none}" | sed -E 's/^ripwire-([0-9a-f]{16})-lean\.bin$/\1/' )" +SIB4="$CD4/ripwire-$ROOTHEX4-rich.bin" +if printf '%s' "$ROOTHEX4" | grep -qE '^[0-9a-f]{16}$'; then + truncate -s 2600M "$SIB4" # this root's own sibling ALONE blows the 2 GB budget (llvm's rich blob is 1.19 GB; a second root doubles it) + ok "(i) primed: root key $ROOTHEX4, sibling family seeded at 2600M (over budget on its own)" +else + no "(i) could not read a 16-hex root key off the primed blob (own='$OWN4')" +fi + +printf 'int solo2( void )\n{\n return 2;\n}\n' >> "$R4/f.cpp" +env -u XDG_CACHE_HOME TMPDIR="$CB4" "$BIN" "$R4" >"$TMP4/run.xml" 2>"$TMP4/run.err" +rc5=$? +[ "$rc5" -eq 0 ] && ok "(i) run exits 0 even with the pinned set over budget" || { no "(i) run exited $rc5"; cat "$TMP4/run.err"; } +grep -q 'n="solo"' "$TMP4/run.xml" 2>/dev/null && ok "(i) run output still correct (solo present)" || no "(i) run output missing solo()" +[ -e "$SIB4" ] && ok "(i) the pinned set is KEPT even though it alone exceeds the budget" \ + || no "(i) the pinned set was evicted when nothing else could be freed — the ping-pong is back" +grep -q '^ripwire: cache ' "$TMP4/run.err" 2>/dev/null && ok "(i) the over-budget pinned set is said once on stderr" \ + || { no "(i) the pinned set exceeded the budget with no disclosure"; cat "$TMP4/run.err"; } +errlines4="$( wc -l < "$TMP4/run.err" | tr -d ' ' )" +[ "$errlines4" -eq 1 ] && ok "(i) exactly ONE stderr line" || no "(i) expected 1 stderr line, got $errlines4" + +# ---- (j) nothing evicted → ZERO stderr bytes ------------------------------------------------------- +# The disclosure must be conditional, or every warm run in every gate that compares stderr grows a line. +TMP5="$( mktemp -d )"; trap 'rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4" "$TMP5"' EXIT +CB5="$TMP5/cachebase"; CD5="$CB5/ripwire"; mkdir -p "$CD5" +R5="$TMP5/repo"; mkdir -p "$R5" +printf 'int quiet( void )\n{\n return 1;\n}\n' > "$R5/f.cpp" +env -u XDG_CACHE_HOME TMPDIR="$CB5" "$BIN" "$R5" >/dev/null 2>/dev/null +printf 'int quiet2( void )\n{\n return 2;\n}\n' >> "$R5/f.cpp" +env -u XDG_CACHE_HOME TMPDIR="$CB5" "$BIN" "$R5" >"$TMP5/run.xml" 2>"$TMP5/run.err" +rc6=$? +[ "$rc6" -eq 0 ] && ok "(j) run exits 0" || no "(j) run exited $rc6" +[ ! -s "$TMP5/run.err" ] && ok "(j) a sweep that evicts nothing writes ZERO bytes to stderr" \ + || { no "(j) stderr is not empty on a no-eviction run — the disclosure is unconditional"; cat "$TMP5/run.err"; } + + [ "$fail" -eq 0 ] && echo "evictioncheck: ALL PASS" || { echo "evictioncheck: SOME CHECKS FAILED"; exit 1; } diff --git a/test/regression.sh b/test/regression.sh index 0d54307d8..07a0d5850 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 5723b2c0808e909c86686a27a2870e16afbfb167 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:31:11 -0400 Subject: [PATCH 14/73] fix(cache): the budget sweep pins the root you are working in, and says what it took MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-1 (highest, 2026-09-10 full audit). One llvm-project root needs 1.76 GB of cache for its OWN two families — rich 1.19 GB + lean 0.57 GB — against a dir-wide 2 GB oldest-first sweep. Add a second corpus, or one --edit-check HEAD snapshot (0.52 GB), and the sweep deletes the SIBLING FAMILY OF THAT SAME ROOT: the one blob the user is certain to need next. Measured, identical argv, same session, same binary: --grep 20 s -> 206 s, --for 19 s -> 268 s, self-sustaining (each cold run's own save evicts the other family again), and SILENT — all four 250 s runs wrote 0 bytes to stderr. `keepPath` never covered it: the blob being written is the family we are NOT about to need. src/main.cpp:181-189 already carries the same mechanism as a registered negative for a different key change. The budget is NOT lowered (owner: quality-first-caps-are-blowup-guards). The ORDER is what changes, and it needs no new state, no plumbing and no extra stat: every family's filename already carries the same 16-hex fnv1a64(realpath(root)) field — lean/rich from defaultCachePath, qheadsnap/qsnap/qbody/qhist/qms/qchurn/stier from shaKeyedCachePath, because headSnapRepoHex hashes exactly the material defaultCachePath does. cacheBlobRootKey reads it off `keepPath`, so whoever is writing IS the most-recently-used root. The size pass then takes other roots first, oldest-first among them, and the pinned set last; if the pinned set alone still exceeds the budget it is kept, and one plain stderr line says so. Both lines are conditional and are plain emits, never DEGRADED_PATH_ALERT — NDEBUG compiles that out and a Release binary is where a 10x slowdown must be visible. The AGE pass stays unpinned deliberately: 30 days untouched is stale by that policy's own definition and costs one cold parse, not a ping-pong. llvm-project (182,555 files, 2.9 GB), private TMPDIR, real blobs, load 16-21. Same starting cache (rich 1,188,813,175 B), same 600 MB other-root filler, same commands: arm before (05f4b892) after output --grep=SmallVector (cold, sweeps) 231.08 s CPU 230.16 s byte-identical -> rich blob after that sweep EVICTED, 0 B stderr KEPT, 1 line --for="how are pass pipelines 274.00 s CPU 26.42 s byte-identical registered" (the next call) (52.38 s wall) (27.06 s) 10.4x P1-11 (high): the same audit read ~30% of a warm llvm --grep as un-reserved vector growth (RawRef/RawDef/RawBind push_back 21.7% of busy + 8.4% memmove) and proposed reserve() before the loops. Reading the code, TWO of the three named paths already reserve exactly — readFileRecord sizes every one of a file's eight families from the record's own count, and mergeThreadFacts reserves each family's exact cross-thread total. The one real omission was the per-thread warm-hit accumulator: it reserved four families of eight, so ffis/routeDefs/routeUses/constOpens doubled up from zero on every warm run. Fixed (the cached FileFacts carry exact counts; reserve(0) is a no-op, so an empty family costs nothing), and made executable: warm_growths= on the RIPWIRE_CACHE_STATS line counts, once per family per file, an append about to cross capacity. THE 30% READING DOES NOT SURVIVE MEASUREMENT, and is not claimed. Warm llvm --grep, 4 interleaved pairs, load 8-19: A 26.79/26.89/26.62/26.42, B 26.67/26.05/26.50/26.23 s CPU — median 26.71 vs 26.37, inside the noise. The residual push_back/memmove share is real but is NOT reallocation: re-sampled after (8 s at 1 ms, 26,160 samples, ~7,923 busy) RawRef push_back 9.2% / RawDef 2.7% / RawBind 2.3% / memmove 4.5% of busy, with warm_growths=49 across 8 families x 18 workers on 81,440 files — i.e. a few dozen reallocations in the whole run, and the rest is the fast-path element move that a reserve cannot remove. golang/go, 11,003 files warm on 18 threads: 29-36. What is left is queue skew (a worker drawing more than its 1/nthreads share), not a missing reserve; covering it would cost 25% more accumulator memory for an effect inside the noise, so it is not done. NON-DEGRADATION. Byte-identical A vs B, 3 corpora x 4 verbs, 12/12 cmp-clean: ripwire's own tree, golang/go (11,003 files), rocksdb — --top-k=100000, --for=..., --grep=reserve, --pack-task=... Determinism (two runs cmp) and xmllint --noout clean; xmlwellformed ALL PASS. Warm default map A/B on go (10 pairs, load 10-13): 0.725 vs 0.710 s CPU median. go --grep (10 pairs): 2.14 vs 2.135. ripwire tree map: 0.12 both. G1: asan/ripwire (address,undefined,integer + LSan suppressions) clean on a cold map, a warm map, and both gates end to end. Gates: evictioncheck (all arms incl. the three new), cachereservecheck, savecachecheck, cacheoffsetcheck, cachesplitcheck, cachefuzzcheck, cacheidentitycheck, cacheisolationcheck, portablecachecheck, headsnapcachecheck, qsnapcachecheck, racymtimecheck, statgatecheck, tornreadcheck, qextractionkeycheck, qschemetripcheck, printffmtparitycheck, limitstablecheck, manifestcheck, gatecountcheck, binoverridecheck, mcpverbscheck, and the rest of --test-gate's 22 named rows — all PASS. --quality-delta: every complexity/verbosity/params/api-surface regression this change first raised was removed by extracting evictBySizeBudget and markCacheHits/reserveWarmFamilies; what remains is short-horizon-churn on the three functions the change must touch, an artifact of the dirty tree against a git-HEAD baseline that disappears once this lands. Nothing acked. KNOWN GAP, stated rather than left to be found: the pin covers blobs that spell the SAME root key. llvm's qchurn blob keys on a different root spelling (6b73c58ba5897c7a vs 4280d3ca01d82374) and is therefore not pinned — 10 MB there, but a family that ever grows large under a divergent root spelling would still be evictable. Co-Authored-By: Claude Fable 5.1 --- src/ingest_parsepool.h | 149 ++++++++++++++++++++++--------- src/quality.h | 193 +++++++++++++++++++++++++++++++++-------- 2 files changed, 266 insertions(+), 76 deletions(-) diff --git a/src/ingest_parsepool.h b/src/ingest_parsepool.h index baf4d4fd3..8f15d9955 100644 --- a/src/ingest_parsepool.h +++ b/src/ingest_parsepool.h @@ -175,8 +175,31 @@ struct PendingParsedFile // an unchanged file's cached facts, re-labelled with today's fileId and appended to the worker's // accumulators — the warm path's whole per-file cost (health is a cached FACT, not a re-derivation). -inline void appendCacheHitFacts( FileFacts& hit, std::uint32_t fileId, IngestFileScan& scan, RawFacts& out ) +// P1-11 (2026-09-10 full audit) — THE WARM-PATH GROWTH OBSERVABLE. §4e of that audit read ~30% of a warm +// llvm `--grep` as un-reserved vector growth on the cache path (RawRef/RawDef/RawBind push_back 21.7% of +// busy + 8.4% memmove). Two of the three suspects were already exact — loadCache reserves each per-file +// family from the record's own count, and mergeThreadFacts reserves each family's exact total — so the +// only accumulator that could still reallocate is THIS one, the per-thread warm-hit accumulator. Counting +// is what settles it: `growths` is incremented once per family per file when the append is about to cross +// capacity, and runParsePool publishes the total as `warm_growths=` on the RIPWIRE_CACHE_STATS line. That +// makes "the warm path does not reallocate" an executable fact instead of a profile reading — the same +// posture `reparsed=`/`cached_records=` already take for their claims. One capacity() compare per family +// per file; nothing in the per-element loops changes. +inline std::size_t appendCacheHitFacts( FileFacts& hit, std::uint32_t fileId, IngestFileScan& scan, RawFacts& out ) { + const auto willGrow = []( const auto& dst, std::size_t need ) noexcept + { + return std::size_t( need > dst.capacity() - dst.size() ); + }; + const std::size_t growths = willGrow( out.defs, hit.defs.size() ) + + willGrow( out.refs, hit.refs.size() ) + + willGrow( out.incs, hit.incs.size() ) + + willGrow( out.binds, hit.binds.size() ) + + willGrow( out.ffis, hit.ffis.size() ) + + willGrow( out.routeDefs, hit.routeDefs.size() ) + + willGrow( out.routeUses, hit.routeUses.size() ) + + willGrow( out.constOpens, hit.constOpens.size() ); + scan.health[ fileId ] = hit.health; // §L1 for( RawDef& d : hit.defs ) { @@ -218,6 +241,77 @@ inline void appendCacheHitFacts( FileFacts& hit, std::uint32_t fileId, IngestFil co.fileId = fileId; out.constOpens.push_back( std::move( co ) ); } + return growths; +} + +// P1-11 — the warm accumulators' EXACT sizing material: every cache hit's per-family fact count, summed. +// Four of these (defs/refs/incs/binds) were already summed inline in runParsePool; the other four were not, +// so ffis/routeDefs/routeUses/constOpens doubled up from zero on every warm run. The cold path skips those +// four ON PURPOSE (coldParseReserve's closing note: it has only a bytes-based ESTIMATE, and estimating a +// family that is empty on most workers is pure waste) — but this path is not estimating. The cached +// FileFacts carry the exact counts, summing them is one more add in a loop that already runs, and +// reserve( 0 ) is a no-op, so an empty family costs nothing and a non-empty one stops reallocating. +struct WarmHitTotals +{ + std::size_t defs = 0, refs = 0, incs = 0, binds = 0; + std::size_t ffis = 0, routeDefs = 0, routeUses = 0, constOpens = 0; +}; + +// One pass over the fileId space: fill `candidates` (path present in the cache, hash not yet compared) and +// `hits` (hash-verified), and total the hits' eight families on the way through. Body moved verbatim out of +// runParsePool, plus the four families it never summed. +inline WarmHitTotals markCacheHits( const std::vector& files, const IngestFileScan& scan, + HashMap& cache, + std::vector& candidates, std::vector& hits ) +{ + WarmHitTotals tot; + for( std::size_t fileId = 0; fileId < files.size(); ++fileId ) + { + const auto it = cache.find( files[ fileId ] ); + if( it == cache.end() ) + { + continue; + } + candidates[ fileId ] = &it->second; + if( it->second.hash != scan.hash[ fileId ] ) + { + continue; + } + hits[ fileId ] = &it->second; + tot.defs += it->second.defs.size(); + tot.refs += it->second.refs.size(); + tot.incs += it->second.incs.size(); + tot.binds += it->second.binds.size(); + tot.ffis += it->second.ffis.size(); + tot.routeDefs += it->second.routeDefs.size(); + tot.routeUses += it->second.routeUses.size(); + tot.constOpens += it->second.constOpens.size(); + } + return tot; +} + +// Each worker gets its 1/nthreads share of every family. Exact when the pool runs one thread; on more, a +// worker that draws more than its share off the lock-free queue still reallocates, which is what +// `warm_growths=` on the RIPWIRE_CACHE_STATS line measures (golang/go, 11,003 files warm on 18 threads: +// 29-36 across 8 families x 18 workers). Ceiling division, so a family with fewer entries than threads +// still gets 1 apiece rather than 0. +inline void reserveWarmFamilies( std::vector& tFacts, const WarmHitTotals& tot, unsigned nthreads ) +{ + const auto share = [ nthreads ]( std::size_t total ) noexcept + { + return ( total + std::size_t( nthreads ) - 1 ) / std::size_t( nthreads ); + }; + for( RawFacts& tf : tFacts ) + { + tf.defs.reserve( share( tot.defs ) ); + tf.refs.reserve( share( tot.refs ) ); + tf.incs.reserve( share( tot.incs ) ); + tf.binds.reserve( share( tot.binds ) ); + tf.ffis.reserve( share( tot.ffis ) ); + tf.routeDefs.reserve( share( tot.routeDefs ) ); + tf.routeUses.reserve( share( tot.routeUses ) ); + tf.constOpens.reserve( share( tot.constOpens ) ); + } } // Everything one parse worker touches, by reference, under one name — the worker function's whole @@ -236,6 +330,7 @@ struct ParsePoolShared std::atomic& nextFile; // lock-free work queue cursor std::atomic& dirty; // any file re-parsed ⇒ cache must be re-saved std::atomic& reparsedCount; // A1 drift observable + std::atomic& warmGrowths; // P1-11: warm-hit accumulator reallocations (RIPWIRE_CACHE_STATS) std::size_t nfiles; bool needsCacheHash; bool captureValueUses; @@ -248,6 +343,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) { IngestFileScan& scan = sh.scan; RawFacts& out = sh.tFacts[ t ]; + std::size_t warmGrowths = 0; // P1-11: thread-local, folded into sh.warmGrowths once at the end (never an atomic in the loop) ParserGuard pg; if( pg.p == nullptr ) @@ -393,7 +489,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) } if( hit != nullptr ) // unchanged → reuse cached facts, skip parse { - appendCacheHitFacts( *hit, std::uint32_t( fileId ), scan, out ); + warmGrowths += appendCacheHitFacts( *hit, std::uint32_t( fileId ), scan, out ); continue; } } @@ -514,6 +610,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) } flushPendingParsed(); ts_query_cursor_delete( cursor ); + sh.warmGrowths.fetch_add( warmGrowths, std::memory_order_relaxed ); // P1-11: one relaxed add per worker, ordered by the pool join } // merge per-thread results into one RawFacts (cross-thread order is irrelevant — everything is @@ -605,6 +702,12 @@ inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::st // counter whose only reader is the post-join print, ordered by the pool join below. std::atomic reparsedCount{ 0 }; + // P1-11: how many times a warm-hit append had to reallocate its accumulator. Zero is the contract on a + // fully warm single-threaded run; a non-zero number on a multi-threaded one is the work-queue's own + // skew (a worker that draws more than its 1/nthreads share), not a missing reserve. Reported only under + // RIPWIRE_CACHE_STATS, like reparsed=/cached_records= beside it. + std::atomic warmGrowths{ 0 }; + const std::size_t nfiles = result.files.size(); if( nfiles ) { @@ -651,41 +754,7 @@ inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::st { PROFILE_SCOPE_DESCRIBE( "ingest/parse-pool: prepare cache-hit reuse" ); - std::size_t hitDefs = 0, hitRefs = 0, hitIncs = 0, hitBinds = 0; - for( std::size_t fileId = 0; fileId < nfiles; ++fileId ) - { - const std::uint64_t h = scan.hash[ fileId ]; - const auto it = cache.find( result.files[ fileId ] ); - if( it == cache.end() ) - { - continue; - } - cacheCandidateFacts[ fileId ] = &it->second; - if( it->second.hash != h ) - { - continue; - } - cacheHitFacts[ fileId ] = &it->second; - hitDefs += it->second.defs.size(); - hitRefs += it->second.refs.size(); - hitIncs += it->second.incs.size(); - hitBinds += it->second.binds.size(); - } - const auto perThreadReserve = [ nthreads ]( std::size_t total ) noexcept - { - return ( total + std::size_t( nthreads ) - 1 ) / std::size_t( nthreads ); - }; - const std::size_t defsPerThread = perThreadReserve( hitDefs ); - const std::size_t refsPerThread = perThreadReserve( hitRefs ); - const std::size_t incsPerThread = perThreadReserve( hitIncs ); - const std::size_t bindsPerThread = perThreadReserve( hitBinds ); - for( unsigned t = 0; t < nthreads; ++t ) - { - tFacts[ t ].defs.reserve( defsPerThread ); - tFacts[ t ].refs.reserve( refsPerThread ); - tFacts[ t ].incs.reserve( incsPerThread ); - tFacts[ t ].binds.reserve( bindsPerThread ); - } + reserveWarmFamilies( tFacts, markCacheHits( result.files, scan, cache, cacheCandidateFacts, cacheHitFacts ), nthreads ); } else { @@ -749,7 +818,7 @@ inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::st std::atomic nextFile{ 0 }; // lock-free work queue: threads fetch_add for the next parseOrder slot ParsePoolShared shared{ result.files, cache, scan, prewarm, queryReadyGate, cacheCandidateFacts, cacheHitFacts, - tFacts, parseOrder, nextFile, dirty, reparsedCount, nfiles, needsCacheHash, captureValueUses }; + tFacts, parseOrder, nextFile, dirty, reparsedCount, warmGrowths, nfiles, needsCacheHash, captureValueUses }; for( unsigned t = 0; t < nthreads; ++t ) { @@ -784,9 +853,9 @@ inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::st if( std::getenv( "RIPWIRE_CACHE_STATS" ) != nullptr ) { const std::size_t reparsed = reparsedCount.load( std::memory_order_relaxed ); - rw::emitTo( stderr, "ripwire: cache-stats reparsed={} reused={} files={} cached_records={} blob_entries={}\n", + rw::emitTo( stderr, "ripwire: cache-stats reparsed={} reused={} files={} cached_records={} blob_entries={} warm_growths={}\n", reparsed, ( nfiles >= reparsed ? nfiles - reparsed : std::size_t( 0 ) ), nfiles, - cacheStats.recordsRead, cacheStats.blobEntries ); + cacheStats.recordsRead, cacheStats.blobEntries, warmGrowths.load( std::memory_order_relaxed ) ); } // Win 2: rewrite cache only when at least one file changed (dirty flag set by workers above). diff --git a/src/quality.h b/src/quality.h index a337c6017..d61b98974 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1642,6 +1642,150 @@ inline std::string headSnapCachePath( const std::string& repoHex, const std::str return shaKeyedCachePath( "qheadsnap", repoHex, exclHex, headSha ); } +// P1-1 (2026-09-10 full audit) — THE PIN KEY. Every cache blob's filename carries the SAME 16-hex root +// field: `defaultCachePath` writes `ripwire--{lean,rich}.bin` and `shaKeyedCachePath` writes +// `ripwire----.bin`, and `headSnapRepoHex` above hashes exactly the +// material `defaultCachePath` does (fnv1a64 of realpath(root)), so ONE root's every family — lean, rich, +// qheadsnap, qsnap, qbody, qhist, qms, qchurn, stier — spells the same key in the same place. That makes +// "which root does this blob belong to?" answerable from the NAME alone, with no plumbing: the byte-budget +// sweep reads the key off the very blob it is about to write (`keepPath`) and pins its siblings. +// +// The rule is positional-free on purpose: return the FIRST '-'-delimited field that is exactly 16 hex +// digits. No family tag is 16 characters of hex ("qheadsnap", "qsnap", "qbody", "qhist", "qms", "qchurn", +// "stier"), so the first such field is the root key in BOTH filename shapes, and a foreign or legacy blob +// that carries no such field yields "" — which pins nothing and evicts exactly as it did before. +inline std::string cacheBlobRootKey( std::string_view blobName ) noexcept +{ + const auto isHex16 = []( std::string_view f ) noexcept + { + if( f.size() != 16 ) + { + return false; + } + for( const char c : f ) + { + if( !std::isxdigit( static_cast( c ) ) ) + { + return false; + } + } + return true; + }; + + std::size_t at = 0; + while( at < blobName.size() ) + { + const std::size_t dash = blobName.find( '-', at ); + const std::string_view field = blobName.substr( at, dash == std::string_view::npos ? std::string_view::npos : dash - at ); + if( isHex16( field ) ) + { + return std::string( field ); + } + if( dash == std::string_view::npos ) + { + break; + } + at = dash + 1; + } + return std::string{}; +} + +// One matching cache artifact as the sweep sees it: what it costs, how old it is, where it is. Hoisted out +// of evictOldCacheFamily's body so the byte-budget pass below can be its own function rather than a third +// in-line pass inside an already-long one. +struct CacheBlobStat +{ + std::filesystem::file_time_type mtime; + std::uintmax_t byteSize; + std::string path; +}; + +// P1-1 (2026-09-10 full audit) — THE BYTE-BUDGET PASS: delete oldest-first until the family is under a +// LOW-WATER mark of 7/8 budget, taking OTHER roots' blobs first and the MRU root's last. Returns the blobs +// that survived. `mine` arrives unsorted; it is sorted oldest-first here. +// +// THE LOW-WATER MARK is F6's live-cache finding (B7.4, 2026-07-14): trimming to exactly the budget left the +// dir hovering AT the ceiling, so every subsequent process re-crossed it on its first write and paid +// deletion work on every save — sweeping to low water buys ~12% burst headroom and makes the common +// next-process sweep a scan-only no-op. +// +// WHOSE BLOB GOES FIRST. Oldest-first alone is wrong at scale, and it was measured wrong: one llvm-project +// root needs 1.76 GB for its OWN two families (rich 1.19 GB + lean 0.57 GB) against a 2 GB budget, so a +// second corpus — or one --edit-check HEAD snapshot (0.52 GB) — made the sweep delete the SIBLING FAMILY OF +// THE ROOT THE USER IS WORKING IN, the one thing they are certain to need next. Identical argv, same +// session, same binary: `--grep` 20 s → 206 s, `--for` 19 s → 268 s, and SELF-SUSTAINING, because each cold +// run's own save then evicts the other family again. `keepPath` alone never covered it: the blob being +// written is precisely the family we are NOT about to need. src/main.cpp:181-189 already records the same +// mechanism as a registered negative for a different key change ("the cache directory's 2 GiB cap evicts the +// blob a running gate is about to reuse"). The BUDGET IS NOT LOWERED (owner rule +// `quality-first-caps-are-blowup-guards`) — the ORDER is what changes, and the pin costs no state and no +// stat: the root key is read off `keepPath`, i.e. whoever is writing IS the most-recently-used root. +// +// WHY ONLY THIS PASS IS PINNED. The age pass stays unpinned deliberately: a blob nobody has touched in 30 +// days is stale by that policy's own definition and losing it costs ONE cold parse, not a ping-pong — +// whereas a blob evicted here is, by construction, one this very root just used. +// +// DISCLOSURE, and the reason P1-1 stayed invisible: all four measured 250 s runs wrote 0 bytes to stderr. +// Conditional by construction — a sweep that frees nothing and is not over budget on its pinned set alone +// says nothing at all, so no ordinary run, and no gate that compares stderr, grows a line. Plain emits, +// NEVER DEGRADED_PATH_ALERT: NDEBUG compiles that out, and a Release binary is exactly where a 10x +// slowdown needs to be visible. +inline std::vector evictBySizeBudget( std::vector& mine, const std::string& dir, + const std::string& keepPath, std::uintmax_t maxTotalBytes ) +{ + namespace fs = std::filesystem; + + std::uintmax_t totalBytes = 0; + for( const CacheBlobStat& b : mine ) + { + totalBytes += b.byteSize; + } + if( totalBytes <= maxTotalBytes ) + { + return std::move( mine ); + } + + const std::string pinRootKey = cacheBlobRootKey( fs::path( keepPath ).filename().string() ); + const std::uintmax_t lowWaterBytes = maxTotalBytes - maxTotalBytes / 8; + std::sort( mine.begin(), mine.end(), []( const CacheBlobStat& a, const CacheBlobStat& b ){ return a.mtime < b.mtime; } ); // oldest first + + std::vector kept; + kept.reserve( mine.size() ); + std::size_t evictedCount = 0; + std::uintmax_t pinnedBytes = 0; + for( const CacheBlobStat& b : mine ) + { + const bool pinned = b.path == keepPath + || ( !pinRootKey.empty() && cacheBlobRootKey( fs::path( b.path ).filename().string() ) == pinRootKey ); + if( pinned ) + { + pinnedBytes += b.byteSize; + } + else if( totalBytes > lowWaterBytes ) + { + std::error_code de; + fs::remove( fs::path( b.path ), de ); + totalBytes -= b.byteSize; + ++evictedCount; + continue; + } + kept.push_back( b ); + } + + constexpr std::uintmax_t kMiB = 1024ull * 1024; + if( evictedCount > 0 ) + { + rw::emitTo( stderr, "ripwire: cache {}: over its {} MiB budget — evicted {} blob(s) of other roots (this root's own families are kept)\n", + dir.c_str(), maxTotalBytes / kMiB, evictedCount ); + } + if( totalBytes > maxTotalBytes ) + { + rw::emitTo( stderr, "ripwire: cache {}: this root's own families are {} MiB, past the {} MiB budget — kept anyway (evicting one costs a full re-parse)\n", + dir.c_str(), pinnedBytes / kMiB, maxTotalBytes / kMiB ); + } + return kept; +} + // Hygiene: within one (repo, excludes) FAMILY, keep at most `keep` HEAD-snapshot cache files (newest by mtime); // delete older ones so HEAD-sha churn (a new file per commit) cannot grow the cache dir without bound. Scoping // per family (repoHex-exclHex prefix) — not per bare repo — means alternating --exclude configs do not evict @@ -1666,14 +1810,17 @@ inline std::string headSnapCachePath( const std::string& repoHex, const std::str // subdirectories, "00".."ff") — so a family's blobs are found and evicted correctly regardless of which // layout wrote them, and a mid-migration mix of both is swept as one set. The 256 shard names are an EXACT, // bounded set (never an open-ended recursive walk of a shared $TMPDIR that may hold unrelated large trees). +// +// P1-1: the byte-budget pass additionally PINS the root `keepPath` belongs to (see evictBySizeBudget). That +// needs no new parameter and no plumbing — the pin key is a function of `keepPath`, which every call site +// already passes — and it cannot reach the keep-N call sites below, which run with maxTotalBytes == 0. inline void evictOldCacheFamily( const std::string& dir, const std::string& prefix, const std::string& keepPath, std::size_t keep, double maxAgeDays = 0.0, std::uintmax_t maxTotalBytes = 0 ) { namespace fs = std::filesystem; - struct Blob { fs::file_time_type mtime; std::uintmax_t byteSize; std::string path; }; - std::vector mine; + std::vector mine; const auto matches = [ & ]( const std::string& name ) { if( name.size() < prefix.size() || name.compare( 0, prefix.size(), prefix ) != 0 ) @@ -1713,7 +1860,7 @@ inline void evictOldCacheFamily( const std::string& dir, const std::string& pref } std::error_code se; const auto sz = sit->file_size( se ); - mine.push_back( Blob{ mt, se ? std::uintmax_t( 0 ) : sz, sit->path().string() } ); // size-stat failure degrades to 0 (age/count passes still see the file) + mine.push_back( CacheBlobStat{ mt, se ? std::uintmax_t( 0 ) : sz, sit->path().string() } ); // size-stat failure degrades to 0 (age/count passes still see the file) } }; @@ -1754,7 +1901,7 @@ inline void evictOldCacheFamily( const std::string& dir, const std::string& pref } std::error_code se; const auto sz = it->file_size( se ); - mine.push_back( Blob{ mt, se ? std::uintmax_t( 0 ) : sz, it->path().string() } ); // size-stat failure degrades to 0 (age/count passes still see the file) + mine.push_back( CacheBlobStat{ mt, se ? std::uintmax_t( 0 ) : sz, it->path().string() } ); // size-stat failure degrades to 0 (age/count passes still see the file) } for( const fs::path& sd : shardDirs ) { @@ -1766,9 +1913,9 @@ inline void evictOldCacheFamily( const std::string& dir, const std::string& pref { const auto ageBudget = std::chrono::duration_cast( std::chrono::duration>( maxAgeDays ) ); const auto cutoff = fs::file_time_type::clock::now() - ageBudget; - std::vector kept; + std::vector kept; kept.reserve( mine.size() ); - for( const Blob& b : mine ) + for( const CacheBlobStat& b : mine ) { if( b.mtime < cutoff && b.path != keepPath ) { @@ -1781,43 +1928,17 @@ inline void evictOldCacheFamily( const std::string& dir, const std::string& pref mine.swap( kept ); } - // size pass: if the family is still over budget, delete oldest-first until under a LOW-WATER mark of - // 7/8 budget (disabled when maxTotalBytes == 0). The hysteresis is F6's live-cache finding (B7.4, - // 2026-07-14): trimming to exactly the budget left the dir hovering AT the ceiling, so every subsequent - // process re-crossed it on its first write and paid deletion work on every save — sweep-to-low-water - // buys ~12% burst headroom and makes the common next-process sweep a scan-only no-op. + // size pass — the byte budget, its eviction order and its disclosure all live in evictBySizeBudget + // above (disabled when maxTotalBytes == 0, which is every keep-N call site below). if( maxTotalBytes > 0 ) { - const std::uintmax_t lowWaterBytes = maxTotalBytes - maxTotalBytes / 8; - std::uintmax_t totalBytes = 0; - for( const Blob& b : mine ) - { - totalBytes += b.byteSize; - } - if( totalBytes > maxTotalBytes ) - { - std::sort( mine.begin(), mine.end(), []( const Blob& a, const Blob& b ){ return a.mtime < b.mtime; } ); // oldest first - std::vector kept; - kept.reserve( mine.size() ); - for( const Blob& b : mine ) - { - if( totalBytes > lowWaterBytes && b.path != keepPath ) - { - std::error_code de; - fs::remove( fs::path( b.path ), de ); - totalBytes -= b.byteSize; - continue; - } - kept.push_back( b ); - } - mine.swap( kept ); - } + mine = evictBySizeBudget( mine, dir, keepPath, maxTotalBytes ); } // count pass (the original behavior): keep only the `keep` newest, delete the rest (disabled via keep == max()). if( keep != std::numeric_limits::max() && mine.size() > keep ) { - std::sort( mine.begin(), mine.end(), []( const Blob& a, const Blob& b ){ return a.mtime > b.mtime; } ); // newest first + std::sort( mine.begin(), mine.end(), []( const CacheBlobStat& a, const CacheBlobStat& b ){ return a.mtime > b.mtime; } ); // newest first for( std::size_t i = keep; i < mine.size(); ++i ) { if( mine[i].path == keepPath ) From d1b1a136c2d706f496b92ed0d6ce260a279d40dd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:33:49 -0400 Subject: [PATCH 15/73] quality(verbosity,complexity): code lines, and growth as a signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two defects, one commit, because they are the same defect seen from both ends: the kinds judged WHERE a number landed and never HOW MUCH this change added. VERBOSITY COUNTED PHYSICAL LINES. Audit lane Q1 added 60 pure BLANK lines inside an 18-LOC body and got `verbosity was="18" now="78"`, gating, exit 2 — and the identical result for 60 pure COMMENT lines, in a repo whose CONTRIBUTING.md requires the reasoning to be written down. Not hypothetical: landed commit 7d5dd201 ("comment(caps): update three stale cap justifications") added 7 comment lines and 1 code line and produced two verbosity rows. Measured composition of what the kind judges over 60 rows: 72.8% code, 23.3% comment, 3.9% blank. The metric is CODE lines now (codeLinesInBody — a documented line heuristic, per-language comment markers, not a lexer; both sides of every comparison run the identical rule). A markdown SECTION keeps its physical span: prose has no code/comment line to separate, and counting its non-blank lines made an in-place docs correction (03ec6f14) read as three verbosity rows. GROWTH WAS NEVER A SIGNAL. `now > was && now > BAR` gated +3% on a 1,068-line function while 6 -> 55 LOC (9x) and ccx 5 -> 13 (+160%) were invisible. Median growth of a gating verbosity row: 6%. Of a gating complexity row: 6%. Over the bar, a row now gates on a CROSSING or on growth >= kMaterialGrowthPct (25%); anything else is real, printed, and sev="minor" — chronic debt the change did not create. Under the bar, a DOUBLING that clears two thirds of the bar is a minor row instead of silence, which is synthetics S4b and S8-sub-bar. `was > 0` is a precondition there and it is load-bearing: growth is a ratio and a new symbol has nothing to double from — without it every added 40-line function reported "grew 4200%", 38 of the first 57 rows this tier produced. params and nesting are untouched (77% precision and no measured false positive respectively; neither moves on a hunch). | | wt before | wt after | ref before | ref after | | rows | 266 | 255 | 259 | 239 | | verbosity rows | 29 | 18 | 60 | 47 | | complexity rows | 11 | 11 | 30 | 31 | | gating rows | 171 | 42 | 69 | 41 | | verbosity gating | 9 | 1 | 19 | 3 | | complexity gating | 6 | 2 | 17 | 5 | | commits that gate | 12/12 | 9/12 | 20/40 | 17/40 | | gating precision TRUE | 2% | 10% | 10% | 17% | | gating precision TRUE+chronic| 16% | 36% | 71% | 51% | (wt = 12 landed commits, working-tree form, cumulative with the churn dial; ref = 40 commits, ack-free, cumulative with the dead-code dial.) ONE TRUE ROW WITHHELD, stated rather than buried: `forLensJsonHeader` (verbosity 56 -> 66) was labelled TRUE by Q1's rule "the symbol CROSSED its bar", and it no longer reports at all. The crossing was of a bar applied to PHYSICAL lines; on code lines the body is still under 60, so by the new measurement nothing crossed. It is the only TRUE row lost in either population. 26 of 27 (wt) and 48 of 49 (ref) TRUE-or-chronic gating rows survive; the 12 and 28 that are demoted are all TRUE-chronic and all still printed, which is exactly what the growth tier is for. Sidecar baseline v4 -> v5 and kQSnapCacheScheme 9 -> 10: locBySym's VALUES changed meaning with the keys untouched, which is what makes a stale one dangerous rather than obvious — it deserializes cleanly, every symbol reads as having SHRUNK, and the verbosity kind reports NOTHING while saying nothing about why. A v4 sidecar is refused by name, like v3 before it. GATE: test/qddialscheck.sh §3, ten arms over one fixture, six of them RED on the pre-change binary — the blank-line and comment-line bodies (gating rows before, no row after), the two over-bar chronic bodies (+5% and +10%, gating before, minor after), and the two sub-bar doublings (silent before, minor rows after) — beside the two crossings that must still gate and the two bar= attributes that must not move. qualitycheck's version-refusal arm follows the wording change. 22 arms, ALL PASS. Co-Authored-By: Claude Fable 5.1 --- src/quality.h | 214 ++++++++++++++++++++++++++++++++++++++--- test/qddialscheck.sh | 69 +++++++++++++ test/qrevtokencheck.sh | 4 +- test/qualitycheck.sh | 4 +- 4 files changed, 273 insertions(+), 18 deletions(-) diff --git a/src/quality.h b/src/quality.h index 097c3f4cc..462d72fa3 100644 --- a/src/quality.h +++ b/src/quality.h @@ -93,6 +93,19 @@ constexpr std::uint32_t kMinorCcxDelta = 3; // complexity: delta < 3 → constexpr std::uint32_t kMinorLocDelta = 10; // verbosity: delta < 10 LOC → minor constexpr std::uint32_t kMinorParamDelta = 2; // params: +1 param → minor; +2 or more → major +// Q-DIAL-3 (2026-09-10) — GROWTH IS A SIGNAL, and the bar alone was not one. `now > was && now > BAR` says +// nothing about how much this change added: audit lane Q1 measured the median growth of a GATING complexity +// row at 6% and of a gating verbosity row at 6% (§2d) — +3% on a function that was 1,068 lines before the +// change gated, while 6 → 55 LOC (9x) and ccx 5 → 13 (+160%) were invisible because neither ends up over the +// bar. Two thresholds fix both halves, and they apply to complexity and verbosity ONLY (params is the +// highest-precision kind in the table at 77% and nesting has no measured false positive — neither is moved +// on a hunch): +constexpr std::uint32_t kMaterialGrowthPct = 25; // over the bar: gate on a bar CROSSING, or on growth >= this. Otherwise the row is real, reported, and sev="minor" — chronic debt the change did not create. +constexpr std::uint32_t kSubBarGrowthPct = 100; // UNDER the bar: a DOUBLING is worth a minor row rather than silence (synthetics S4b/S8) — never gating, because nothing is over the bar yet. +// …with a floor so a 3 → 6 line helper is not a finding. Two thirds of the kind's own bar, so the floor moves +// with the bar it belongs to and there is no third number to keep in sync: ccx 10, loc 40. +inline constexpr std::uint32_t subBarGrowthFloor( std::uint32_t bar ) noexcept { return ( bar * 2 ) / 3; } + // Signal-to-noise round — the per-finding ACK RATCHET sidecar (`--quality-ack[=REASON]`): each line records one // deliberately-accepted finding; --quality-delta suppresses it (honestly, via acked="N") until the finding // WORSENS past the acked magnitude, at which point it reappears. Committable, like the baseline sidecar. @@ -166,7 +179,7 @@ inline std::string baselineCanonId( const IngestResult& ing, NodeId i, std::stri struct Snapshot { gtl::btree_map ccxBySym; // hash(canonId) → MAX ccx (btree = sorted iteration for the byte-stable sidecar) - gtl::btree_map locBySym; // Q1 verbosity — hash(canonId) → MAX physical LOC (the master variable, §1d) + gtl::btree_map locBySym; // Q1 verbosity — hash(canonId) → MAX CODE lines (Q-DIAL-3: blank and comment-only lines are not debt; see codeLinesInBody). ALSO the r26 ORIGIN oracle, which reads MEMBERSHIP only, so the value change does not touch it. gtl::btree_map nestBySym; // Q1 erosion — hash(canonId) → MAX control-nesting depth gtl::btree_map paramsBySym; // Q1 erosion — hash(canonId) → MAX parameter count gtl::btree_map defsBySym; // hash(canonId) → COUNT of definitions sharing the id (an overload set's CARDINALITY, deliberately NOT a MAX — see computeSnapshot) @@ -768,6 +781,137 @@ inline void forEachSymbolBody( const IngestResult& ing, Fn&& visit ) } } +// Q-DIAL-3 (2026-09-10) — THE VERBOSITY KIND'S METRIC: CODE lines, not physical lines. +// +// `Symbol::loc` is the def's physical line span, and the verbosity kind judged it directly. That makes blank +// lines and comments debt: audit lane Q1 added 60 PURE BLANK lines inside an 18-LOC body and got +// `verbosity was="18" now="78"`, gating, exit 2 — and the same for 60 pure COMMENT lines, in a repo whose own +// CONTRIBUTING.md requires the reasoning to be written down. It is not hypothetical either: landed commit +// 7d5dd201 ("comment(caps): update three stale cap justifications") added 7 comment lines and 1 code line and +// produced two verbosity regression rows. Measured composition of what the kind judges, over 60 rows: +// 72.8% code, 23.3% comment, 3.9% blank. +// +// A LINE HEURISTIC, NOT A LEXER, and the floor is stated rather than implied: a line counts as code unless it +// is blank or its first non-space characters open a comment. So a trailing comment after code counts as code +// (correct), a comment marker inside a string literal makes that line read as a comment (wrong, and rare), and +// a multi-line raw string full of blank lines reads as blank (wrong, and rarer). The alternative is a second +// tokenization pass per symbol on every --quality-delta, for a metric whose whole job is to say "this body is +// big". Both sides of every comparison run the identical rule, which is the property the delta actually needs. +// +// Markers by language family, from the symbol's own `lang`: `//` plus `/* … */` for the C family and its +// descendants, `#` for the shell/Python/Ruby/Elixir/config family (in the C family `#` opens a PREPROCESSOR +// directive, which is code — that is why this is per-language and not one union set), `--` for Lua. Markdown +// and JSON have no comment syntax, so every non-blank line there is content. +inline bool langUsesHashComment( Lang l ) noexcept +{ + return l == Lang::Python || l == Lang::Bash || l == Lang::Ruby || l == Lang::Elixir + || l == Lang::Toml || l == Lang::Yaml; +} + +inline std::uint32_t codeLinesInBody( std::string_view body, Lang lang ) noexcept +{ + const bool hash = langUsesHashComment( lang ); + const bool cLike = !hash && lang != Lang::Markdown && lang != Lang::Json && lang != Lang::Lua; + const bool lua = lang == Lang::Lua; + std::uint32_t code = 0; + bool inBlock = false; + std::size_t at = 0; + while( at <= body.size() ) + { + const std::size_t nl = body.find( '\n', at ); + std::string_view line = body.substr( at, ( nl == std::string_view::npos ? body.size() : nl ) - at ); + at = ( nl == std::string_view::npos ) ? body.size() + 1 : nl + 1; + while( !line.empty() && ( line.front() == ' ' || line.front() == '\t' || line.front() == '\r' ) ) + { + line.remove_prefix( 1 ); + } + while( !line.empty() && ( line.back() == ' ' || line.back() == '\t' || line.back() == '\r' ) ) + { + line.remove_suffix( 1 ); + } + if( inBlock ) + { + const std::size_t close = line.find( "*/" ); + if( close == std::string_view::npos ) + { + continue; // still inside the block comment + } + inBlock = false; + line.remove_prefix( close + 2 ); + while( !line.empty() && ( line.front() == ' ' || line.front() == '\t' ) ) + { + line.remove_prefix( 1 ); + } + } + if( line.empty() ) + { + continue; // blank + } + if( cLike && line.rfind( "//", 0 ) == 0 ) + { + continue; + } + if( hash && line.front() == '#' ) + { + continue; + } + if( lua && line.rfind( "--", 0 ) == 0 ) + { + continue; + } + if( cLike && line.rfind( "/*", 0 ) == 0 ) + { + inBlock = line.find( "*/", 2 ) == std::string_view::npos; + if( !inBlock ) + { + const std::size_t close = line.find( "*/", 2 ); + std::string_view rest = line.substr( close + 2 ); + while( !rest.empty() && ( rest.front() == ' ' || rest.front() == '\t' ) ) + { + rest.remove_prefix( 1 ); + } + if( rest.empty() ) + { + continue; // `/* … */` alone on the line + } + } + else + { + continue; + } + } + ++code; + } + return code; +} + +// The per-NODE code-line count for THIS tree, read off each symbol's own body bytes in ONE pass over the +// files (forEachSymbolBody). A symbol with no readable body — a declaration, a prototype, an unreadable file — +// keeps its physical `loc`: that span IS its signature, there is nothing to discount, and a silent 0 there +// would read as "this symbol shrank to nothing" on the next delta. +inline std::vector codeLocByNode( const IngestResult& ing ) +{ + std::vector out( ing.symbols.size(), 0 ); + for( NodeId i = 0; i < ing.symbols.size(); ++i ) + { + out[i] = ing.symbols[i].loc; + } + forEachSymbolBody( ing, [ & ]( NodeId i, const Symbol& s, std::string_view body ) + { + if( s.kind == SymKind::Section ) + { + return; // a markdown SECTION is prose: there is no code/comment line to separate, and counting + // its non-blank lines as "code" makes an in-place doc rewrite that swaps 5 blank lines + // for 5 sentences read as +5 verbosity. Measured on the ref-pair replay before this + // clause: 03ec6f14 (a docs correction) went from a clean report to three minor rows. + // Sections keep the physical span they always had — the churn kind exempts them for the + // same reason ("doc sections churn by design"). + } + out[i] = codeLinesInBody( body, s.lang ); + } ); + return out; +} + // P2.2 — every symbol in THIS tree whose own signature text is a registered-macro call (built ONCE per // computeSnapshot/computeDelta run, exactly like topLevelCallees above), reading each file's bytes once via // forEachSymbolBody — whose per-symbol `body` view already starts at sigStartByte, which is precisely where @@ -2090,7 +2234,11 @@ inline void evictOldHeadSnapCaches( const std::string& dir, const std::string& r // dead — a whole tree of phantom regressions on the first run after an upgrade. No extraction change (the // symbols were always indexed; only the dead-SET predicate moved), so kParserVer and its mirrors deliberately // did NOT move. Bumped 8 -> 9 to retire every blob written before it. -constexpr std::uint32_t kQSnapCacheScheme = 9; +// v10 (Q-DIAL-3, 2026-09-10) — locBySym's VALUES are CODE lines now, not the physical span. Keys unchanged, +// which is exactly what makes a stale blob dangerous rather than obvious: a v9 blob deserializes cleanly and +// every symbol reads as having SHRUNK (its recorded physical loc exceeds the current code count), so the +// verbosity kind reports NOTHING and says nothing about why. Bumped 9 -> 10. +constexpr std::uint32_t kQSnapCacheScheme = 10; constexpr char kQSnapMagic[4] = { 'Q', 'S', 'N', 'P' }; // The qsnap EXCLUDES-config key folds the qsnap SCHEME (independent of the ingest cache's kHeadSnapCacheScheme) @@ -3006,6 +3154,7 @@ inline std::vector> gitCoChangeAndChurnCached( inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::string_view root = {} ) { Snapshot snap; + const std::vector codeLoc = codeLocByNode( ing ); // Q-DIAL-3: the verbosity kind's metric is CODE lines const std::vector topLevelCallees = topLevelCalleeNameHashes( ing ); // W1-S2: dead-kind evidence, built once const std::vector macroNames = registeredMacroNames( root ); // P2.2: built-ins + .ripwire_config const std::vector macroIds = registeredMacroSymbolIds( ing, macroNames ); @@ -3021,7 +3170,7 @@ inline Snapshot computeSnapshot( const IngestResult& ing, const Graph& g, std::s // last-writer-wins; otherwise a low-metric overload written last makes every later delta report a // phantom regression forever (THE trap). Every new per-symbol kind mirrors this MAX exactly. { std::uint32_t& slot = snap.ccxBySym[ key ]; slot = std::max( slot, s.ccx ); } - { std::uint32_t& slot = snap.locBySym[ key ]; slot = std::max( slot, s.loc ); } + { std::uint32_t& slot = snap.locBySym[ key ]; slot = std::max( slot, codeLoc[i] ); } // Q-DIAL-3: CODE lines, not the physical span { std::uint32_t& slot = snap.nestBySym[ key ]; slot = std::max( slot, std::uint32_t( s.maxNest ) ); } { std::uint32_t& slot = snap.paramsBySym[ key ]; slot = std::max( slot, std::uint32_t( s.params ) ); } // THE ONE KIND THAT IS NOT A MAX, and the reason is the MAX itself. Every metric above collapses the @@ -3088,7 +3237,12 @@ inline bool writeBaseline( const Snapshot& s, const std::string& path, std::stri // v4 (2026-08-25): every per-symbol key is pathQualifiedKey, not fnv1a64(baselineCanonId). readBaseline // REFUSES v3 and older rather than reading it — see there for why a silent read would be the dishonest // option here. - f << "# ripwire quality baseline v4 — regenerate with --quality-baseline; do not hand-edit\n"; + // v5 (Q-DIAL-3, 2026-09-10): the `loc` record's VALUE changed meaning — CODE lines, not the physical span + // (codeLinesInBody). The key space is untouched, so a v4 sidecar would read perfectly and be WRONG in one + // direction only: its loc values are larger, every symbol reads as having SHRUNK, and the verbosity kind + // silently reports nothing at all. A kind that quietly stops firing is the worst of the three outcomes, so + // this is a version refusal like v4's, not a graceful skip. + f << "# ripwire quality baseline v5 — regenerate with --quality-baseline; do not hand-edit\n"; // STALENESS STAMP: the HEAD commit the baseline was pinned at. --quality-delta compares this to the // current HEAD and, if they differ (a baseline left by an abandoned/parallel session, or from before a // commit), IGNORES the sidecar and falls back to the git-HEAD auto-baseline instead of reporting a wall @@ -3166,7 +3320,7 @@ inline bool writeBaseline( const Snapshot& s, const std::string& path, std::stri // cost of refusing is one `--quality-baseline` re-pin. inline bool baselineHeaderIsForeign( const std::string& line ) noexcept { - return line.rfind( "# ripwire quality baseline v", 0 ) == 0 && line.find( " v4 " ) == std::string::npos; + return line.rfind( "# ripwire quality baseline v", 0 ) == 0 && line.find( " v5 " ) == std::string::npos; } // 2026-09-06 stranger audit: the sidecar readers dropped what they could not parse with no trace a Release @@ -3203,7 +3357,7 @@ inline bool readBaseline( const std::string& path, Snapshot& out, BaselineReadSt // The refusal is a USER-FACING disclosure, so it must survive NDEBUG: behind only a // DEGRADED_PATH_ALERT a Release binary refuses SILENTLY and the caller reads "no baseline // found" — a refusal that hides its reason misleads exactly like the misread it prevents. - rw::emitRaw( stderr, "ripwire: quality: baseline sidecar predates the pathQualifiedKey scheme — refused, re-pin with --quality-baseline\n" ); + rw::emitRaw( stderr, "ripwire: quality: baseline sidecar predates this binary's baseline format — refused, re-pin with --quality-baseline\n" ); out = Snapshot{}; return false; } @@ -5666,8 +5820,18 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // the trap is handled the same way for every one of them. // `minorDelta` is the kind's materiality tier: a regression whose growth (now − was) is under it is // reported sev="minor" and does not gate exit 2 (0 = no tier, every regression is major). + // + // Q-DIAL-3 — `growthTiered` swaps that flat delta tier for the pair of thresholds kMaterialGrowthPct / + // kSubBarGrowthPct define, for complexity and verbosity only: + // OVER the bar — gate on a bar CROSSING (was <= bar < now) or on growth >= 25%; anything else is a + // real row, printed, sev="minor". It names debt the change did not create. + // UNDER the bar — a DOUBLING that clears the floor is a minor row instead of silence. Nothing here can + // gate: the symbol is still under its bar, and the row exists to be seen, not to stop + // a commit. + // `metricOf` takes the NodeId rather than the Symbol because verbosity's metric is not on the Symbol any + // more (codeLoc is read off the body bytes); the other three still just read a field. const auto perSymbolKind = - [ & ]( const char* kindName, std::uint32_t bar, std::uint32_t minorDelta, + [ & ]( const char* kindName, std::uint32_t bar, std::uint32_t minorDelta, bool growthTiered, const gtl::btree_map& baseMap, auto metricOf ) { @@ -5679,7 +5843,7 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap continue; } std::uint32_t& slot = nowBySym[ keyByNode[i] ]; - slot = std::max( slot, metricOf( ing.symbols[i] ) ); + slot = std::max( slot, metricOf( i ) ); } ScratchMap reported( ing.symbols.size() ); for( NodeId i = 0; i < ing.symbols.size(); ++i ) @@ -5701,19 +5865,41 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap const std::uint32_t now = nowIt->second; const auto it = baseMap.find( key ); const std::uint32_t was = ( it == baseMap.end() ) ? 0u : it->second; - if( now > was && now > bar ) + if( now <= was ) + { + continue; // nothing got worse on this axis + } + const std::uint64_t growthPct = ( std::uint64_t( now - was ) * 100 ) / std::max( was, 1u ); + if( now > bar ) { - regs.push_back( { kindName, g.canonId[i], was, now, key, minorDelta > 0 && now - was < minorDelta, + const bool crossed = was <= bar; + const bool material = !growthTiered ? ( minorDelta == 0 || now - was >= minorDelta ) + : ( crossed || growthPct >= kMaterialGrowthPct ); + regs.push_back( { kindName, g.canonId[i], was, now, key, !material, {}, !existedAtBaseline( key ) } ); // origin: the finding IS this symbol stampLoc( i ); } + else if( growthTiered && was > 0 && growthPct >= kSubBarGrowthPct && now >= subBarGrowthFloor( bar ) ) + { + // Q-DIAL-3 — still UNDER the bar, so this can never gate; it is the row that turns synthetics + // S4b (6 → 55 LOC) and S8-sub-bar (ccx 5 → 13) from silence into something a reader can see. + // `was > 0` is load-bearing, not defensive: growth is a RATIO and a brand-new symbol has + // nothing to double from, so without it every added function of 40 code lines or ccx 10 + // reported as "grew 4200%". Measured on the 40-commit ref-pair replay: 38 of the 57 rows this + // tier first produced were exactly that (`was="0"`), including every symbol of the vendored + // timsort landing at 08416403. + regs.push_back( { kindName, g.canonId[i], was, now, key, /*isMinor=*/true, + {}, !existedAtBaseline( key ) } ); + stampLoc( i ); + } } }; - perSymbolKind( "complexity", kCcxBar, kMinorCcxDelta, base.ccxBySym, []( const Symbol& s ){ return s.ccx; } ); - perSymbolKind( "verbosity", kLocBar, kMinorLocDelta, base.locBySym, []( const Symbol& s ){ return s.loc; } ); - perSymbolKind( "nesting", kNestBar, 0, base.nestBySym, []( const Symbol& s ){ return std::uint32_t( s.maxNest ); } ); - perSymbolKind( "params", kParamBar, kMinorParamDelta, base.paramsBySym, []( const Symbol& s ){ return std::uint32_t( s.params ); } ); + const std::vector nowCodeLoc = codeLocByNode( ing ); // Q-DIAL-3 — the same rule computeSnapshot recorded the baseline with + perSymbolKind( "complexity", kCcxBar, kMinorCcxDelta, true, base.ccxBySym, [ & ]( NodeId i ){ return ing.symbols[i].ccx; } ); + perSymbolKind( "verbosity", kLocBar, kMinorLocDelta, true, base.locBySym, [ & ]( NodeId i ){ return nowCodeLoc[i]; } ); + perSymbolKind( "nesting", kNestBar, 0, false, base.nestBySym, [ & ]( NodeId i ){ return std::uint32_t( ing.symbols[i].maxNest ); } ); + perSymbolKind( "params", kParamBar, kMinorParamDelta, false, base.paramsBySym, [ & ]( NodeId i ){ return std::uint32_t( ing.symbols[i].params ); } ); // PERF (P5W2) — the working-tree clone pass is the dominant --quality-delta cost: on a large private C++ corpus the // Type-3 pass alone is ~2.7-3.2 s (60 M intra-bucket pair-visits; tokenization is only ~3 %). It is a PURE diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 9a651964e..96375416c 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -132,5 +132,74 @@ rows "$ODC" | grep 'kind="dead-code"' | grep -q 'Extra' \ && ok "dead-code: byte-identical run to run (deterministic)" || no "dead-code: non-deterministic delta" +# ── 3) verbosity counts CODE lines; verbosity + complexity gate on a CROSSING or >= 25% growth ─────────── +# Q1 measured the median growth of a GATING verbosity row at 6% and of a gating complexity row at 6%, while +# 60 pure BLANK lines added to an 18-LOC body produced `was="18" now="78"`, gating, exit 2. Both halves are +# fixed here: the metric stops counting blank and comment-only lines, and the severity asks how much this +# change ADDED rather than only where the number landed. One fixture, one working edit, six shapes. +VB="$WORK/verb"; mkdir -p "$VB/src" +( cd "$VB" && git init -q && git config user.email t@t && git config user.name t && git config commit.gpgsign false ) +gen(){ python3 - "$VB/src/v.cpp" "$1" <<'PY' +import sys +p, stage = sys.argv[1], sys.argv[2] +def body(n): # n CODE lines inside the braces + return "".join(" x += %d;\n" % (i % 7 + 1) for i in range(n)) +def ifs(n): # n sequential ifs at depth 0 => cognitive complexity n + return "".join(" if( x == %d ) { x += 1; }\n" % i for i in range(n)) +after = stage == "after" +out = [] +out.append("int blankGrow( int x ){\n" + body(10) + ("\n"*60 if after else "") + " return x;\n}\n") +out.append("int commentGrow( int x ){\n" + body(10) + ("".join(" // note %d\n" % i for i in range(60)) if after else "") + " return x;\n}\n") +out.append("int crosser( int x ){\n" + body(70 if after else 50) + " return x;\n}\n") +out.append("int chronic( int x ){\n" + body(210 if after else 200) + " return x;\n}\n") +out.append("int doubler( int x ){\n" + body(45 if after else 20) + " return x;\n}\n") +out.append("int cxDoubler( int x ){\n" + ifs(13 if after else 5) + " return x;\n}\n") +out.append("int cxChronic( int x ){\n" + ifs(33 if after else 30) + " return x;\n}\n") +out.append("int cxCrosser( int x ){\n" + ifs(20 if after else 10) + " return x;\n}\n") +open(p, "w").write("".join(out)) +PY +} +gen before +( cd "$VB" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) +gen after +OVB="$( cd "$VB" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +vrow(){ row "$OVB" "$1" "$2"; } +# (a) BLANK and COMMENT lines are not debt — the two synthetics that gated before. +for f in blankGrow commentGrow; do + vrow verbosity "$f" >/dev/null \ + && { no "verbosity: $f reported — blank/comment lines are still counted as debt"; vrow verbosity "$f"; } \ + || ok "verbosity: $f produces no row (60 blank/comment lines are not code)" +done +# (b) a CROSSING still gates, on both kinds — the shape commit 65d98b76 was written to clear. +vrow verbosity crosser | grep -q 'gating="1"' \ + && ok "verbosity: crosser 50 -> 70 code lines CROSSES the bar and gates" \ + || { no "verbosity: a bar crossing must still gate"; vrow verbosity crosser; } +vrow complexity cxCrosser | grep -q 'gating="1"' \ + && ok "complexity: cxCrosser ccx 10 -> 20 CROSSES the bar and gates" \ + || { no "complexity: a bar crossing must still gate"; vrow complexity cxCrosser; } +# (c) over the bar but grew under 25% — a real row, printed, minor, not gating. +vrow verbosity chronic | grep -q 'sev="minor"' \ + && ok "verbosity: chronic 200 -> 210 (+5%) is sev=minor, not a gate" \ + || { no "verbosity: +5% on an already-huge body must not gate"; vrow verbosity chronic; } +vrow complexity cxChronic | grep -q 'sev="minor"' \ + && ok "complexity: cxChronic 30 -> 33 (+10%) is sev=minor, not a gate" \ + || { no "complexity: +10% on an already-complex body must not gate"; vrow complexity cxChronic; } +# (d) UNDER the bar, a doubling is a minor row instead of silence (synthetics S4b / S8-sub-bar). +vrow verbosity doubler | grep -q 'sev="minor"' \ + && ok "verbosity: doubler 20 -> 45 code lines (under the bar, +125%) is a minor row" \ + || { no "verbosity: a sub-bar doubling should be a minor row (S4b)"; vrow verbosity doubler; } +vrow complexity cxDoubler | grep -q 'sev="minor"' \ + && ok "complexity: cxDoubler ccx 5 -> 13 (under the bar, +160%) is a minor row" \ + || { no "complexity: a sub-bar doubling should be a minor row (S8)"; vrow complexity cxDoubler; } +vrow verbosity doubler | grep -q 'gating="1"' \ + && no "verbosity: a sub-bar row must never gate — nothing is over the bar yet" \ + || ok "verbosity: the sub-bar row does not gate" +# bar= semantics are unchanged: it still names the kind's own threshold. +vrow verbosity crosser | grep -q 'bar="60"' && ok "verbosity: bar=60 unchanged" || no "verbosity: bar= moved" +vrow complexity cxCrosser | grep -q 'bar="15"' && ok "complexity: bar=15 unchanged" || no "complexity: bar= moved" +[ "$OVB" = "$( cd "$VB" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "verbosity/complexity: byte-identical run to run (deterministic)" || no "verbosity/complexity: non-deterministic delta" + + [ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" exit "$fail" diff --git a/test/qrevtokencheck.sh b/test/qrevtokencheck.sh index 6e929e489..22eee87a8 100755 --- a/test/qrevtokencheck.sh +++ b/test/qrevtokencheck.sh @@ -64,7 +64,7 @@ NOSIDECAR="$( run )"; NOSIDECAR_RC="$( rc_of )" # ── (a)(b) hostile `head` stamps ─────────────────────────────────────────────────────────────────────────── BASEF="$REPO/.ripwire_quality_baseline" for payload in "--output=$VICTIM" "-e" "--upload-pack=touch $WORK/pwned" "$WORK/../victim.txt" "HEAD; touch $WORK/pwned2"; do - printf '# ripwire quality baseline v4 — regenerate with --quality-baseline; do not hand-edit\nhead %s\nloc deadbeef 10\n' "$payload" > "$BASEF" + printf '# ripwire quality baseline v5 — regenerate with --quality-baseline; do not hand-edit\nhead %s\nloc deadbeef 10\n' "$payload" > "$BASEF" OUT="$( run )"; RC="$( rc_of )" [ "$( cat "$VICTIM" )" = "$VICTIM_BEFORE" ] \ && ok "hostile head stamp '$payload': the file outside the repo is untouched" \ @@ -80,7 +80,7 @@ done # The tampered sidecar must be DISTRUSTED: it routes into the existing unreachable-pin self-heal, so the # FINDINGS equal the no-sidecar git-HEAD answer, and the header says out loud that the sidecar was removed # (never a silent substitution — the baseline= marker is the audit trail). -printf '# ripwire quality baseline v4 — regenerate with --quality-baseline; do not hand-edit\nhead --output=%s\nloc deadbeef 10\n' "$VICTIM" > "$BASEF" +printf '# ripwire quality baseline v5 — regenerate with --quality-baseline; do not hand-edit\nhead --output=%s\nloc deadbeef 10\n' "$VICTIM" > "$BASEF" TAMPERED="$( run )" rows_of(){ printf '%s' "$1" | tr '<' '\n' | grep '^r kind='; } [ "$( rows_of "$TAMPERED" )" = "$( rows_of "$NOSIDECAR" )" ] \ diff --git a/test/qualitycheck.sh b/test/qualitycheck.sh index 3f6c5c7be..ffc69a32d 100755 --- a/test/qualitycheck.sh +++ b/test/qualitycheck.sh @@ -146,8 +146,8 @@ case "$V1EC" in *) no "pre-v4 baseline crashed (exit $V1EC)" ;; esac case "$V1OUT" in - *"predates the pathQualifiedKey scheme"*) ok "the pre-v4 sidecar is REFUSED by name, not silently misread" ;; - *) no "a pre-v4 sidecar was consumed without a refusal — every symbol would read as new debt: $( printf '%s' "$V1OUT" | head -c 160 )" ;; + *"predates this binary's baseline format"*) ok "the outdated sidecar is REFUSED by name, not silently misread" ;; + *) no "an outdated sidecar was consumed without a refusal — every symbol would read as new debt: $( printf '%s' "$V1OUT" | head -c 160 )" ;; esac # (b) WITH git history the refusal must land on the disclosed git-HEAD fallback rather than on nothing. if command -v git >/dev/null 2>&1; then From d6371f76e6de619dab77e727eb5faab84fb8fe4e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:48:05 -0400 Subject: [PATCH 16/73] docs(lexindex,subtokencheck): the comments say "fused rolling hash" about code that no longer rolls Comment-only. The hashed walker stopped being a second copy of the state machine with a rolling FNV inside it at 33ea1499; it is now one block walk and a per-token fold, so the two places that describe it as "the fused rolling hash" describe code that is not there. lexSubtokenHash gains the note that matters more than the wording: it KEEPS its range test on purpose. The hashed walker folds with the branchless `c | ( ( c & 0x40 ) >> 1 )`, which is exact for [A-Za-z0-9] and wrong for anything else ('@' would fold to '`'), and this entry point is the one external callers reach with bytes nothing has classified. Re-verified after the edit: 18/18 byte-identical over three corpora x six verbs, strkerncheck, subtokencheck and g1freshcheck green, --quality-delta regressions=0 gating=0. --- src/lexindex.h | 5 ++++- test/subtokencheck.sh | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/lexindex.h b/src/lexindex.h index 6ca28ff0d..17739f413 100644 --- a/src/lexindex.h +++ b/src/lexindex.h @@ -227,7 +227,10 @@ inline void forEachLexSubtoken( std::string_view text, EmitFn&& emit ) // "MCP", and hashing that as "mCP" would make the postings path miss the query token "mcp" that the scan // path matches. 64-bit keys make a cross-token collision (the only other way the postings path could // diverge from the scan path) astronomically unlikely; the postingscheck equivalence gate verifies -// byte-identity on the real corpora, and test/subtokencheck.sh arm C pins this against the fused walker. +// byte-identity on the real corpora, and test/subtokencheck.sh arm C pins this against the hashed walker. +// KEEP THE RANGE TEST HERE. The hashed walker uses the branchless `c | ( ( c & 0x40 ) >> 1 )` fold, which +// is exact for [A-Za-z0-9] and WRONG for anything else ('@' would become '`'); this entry point is the one +// external callers reach with bytes nothing has classified, so it stays general. inline std::uint64_t lexSubtokenHash( const char* tok, std::size_t tokLen ) noexcept { std::uint64_t h = 1469598103934665603ull; diff --git a/test/subtokencheck.sh b/test/subtokencheck.sh index 7417880c5..dfb3765a0 100755 --- a/test/subtokencheck.sh +++ b/test/subtokencheck.sh @@ -26,7 +26,7 @@ # (B) unit, MIRROR EQUIVALENCE — lexindex.h's forEachLexSubtoken() (the corpus-side walker BM25 # actually scans with) must yield exactly the token list subtokens() yields, over every case in # the table. This is the arm that stops the three copies drifting apart again. -# (C) unit, HASH PARITY — forEachLexSubtokenHashed()'s fused rolling hash must equal +# (C) unit, HASH PARITY — forEachLexSubtokenHashed()'s per-token hash must equal # lexSubtokenHash() of the token's lowercased bytes. A token may now carry INTERIOR uppercase, # which is exactly the input that used to normalize differently on the two paths; if this arm # is red, the persisted postings path and the query-time scan disagree. @@ -168,7 +168,9 @@ int main() } } - // (C) the fused rolling hash must equal lexSubtokenHash() of the token's lowercased bytes + // (C) the walker's per-token hash must equal lexSubtokenHash() of the token's lowercased bytes + // (it was a rolling hash fused into a second copy of the state machine until 2026-09-10; the walkers + // now share ONE block walk and the hash runs over the span, so this arm pins the FOLD, not the roll) for( const Row& r : kRows ) { const std::string_view text( r.in ); @@ -205,7 +207,7 @@ EOF else grep -q '^A-MISMATCH' "$TMP/subtok.out" && no "unit (A) subtokens() disagrees with the registered rule" || ok "unit (A) subtokens() splits all five shapes per the registered rule" grep -q '^B-MIRROR-DRIFT' "$TMP/subtok.out" && no "unit (B) forEachLexSubtoken() has drifted from subtokens()" || ok "unit (B) forEachLexSubtoken() mirrors subtokens() token-for-token" - grep -q '^C-HASH-DRIFT' "$TMP/subtok.out" && no "unit (C) the fused rolling hash disagrees with lexSubtokenHash()" || ok "unit (C) forEachLexSubtokenHashed() agrees with lexSubtokenHash() on every token" + grep -q '^C-HASH-DRIFT' "$TMP/subtok.out" && no "unit (C) the walker's per-token hash disagrees with lexSubtokenHash()" || ok "unit (C) forEachLexSubtokenHashed() agrees with lexSubtokenHash() on every token" sed -n '1,24p' "$TMP/subtok.out" fi else From ec90bc0c68986892f7a9061e89530f7e00773ee2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:48:16 -0400 Subject: [PATCH 17/73] quality(api-surface): a count for new exports, and one row per fact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four findings from audit lane Q1's replay, all on one kind, all the same mistake in different clothes: the kind reported everything it could SEE about the public surface instead of what a change made WORSE. (1) 103 of 119 api-surface rows over 40 replayed commits carried origin="new-symbol" — a row per new export, which the legend itself says can never gate. 193 of the 1,177 rows in this repo's own committed ack ledger are that shape, acked by hand one at a time for a fact the header can state in one attribute. They are api-new-surface="N" on the root now: never gating, never counted in regressions=, printed even at zero. (2) Three rows over 40 commits reported an arity DROP as a regression (probeBodyCost 7->5, selectMonotoneBodySubset 7->5, liftPackageDirMention 4->3) in a document whose first sentence is "only what a change made WORSE". A smaller surface is no longer a row. (3) 113 of this repo's 132 api-surface acks say the same sentence: "one trailing DEFAULTED parameter, every existing caller compiles unchanged". That shape is read off the signature (trailingParamHasDefault — a documented brace-depth scan, not a parser) and reported sev="minor". Still a row: the contract did move. (4) One parameter change emitted TWO rows, under `params` and again under `api-surface` (synthetic S3, 3 -> 7 parameters). The arity row folds into `params`, which at 77% TRUE is the highest-precision kind in the table. | | wt before | wt after | ref before | ref after | | rows | 266 | 209 | 259 | 127 | | api-surface rows | 52 | 6 | 119 | 7 | | gating rows | 171 | 32 | 69 | 26 | | api-surface gating | 11 | 1 | 16 | 1 | | commits that gate | 12/12 | 8/12 | 20/40 | 13/40 | | gating precision TRUE | 2% | 12% | 10% | 27% | | WRONG rows (gating) | 1 | 0 | 6 | 3 | (cumulative with the three dials before it.) NO FACT IS LOST. Five gating api-surface rows disappear in the working-tree population and six in the ref-pair one; every single one has a surviving `params` row carrying the identical was/now (editCheckBundleText 8->10 and editpreview::run 10->12 still gating, gitLogFileSets/gitRecentCommitFileSets 5->6 and packSignatures 20->21 and packBodies 14->15 now minor). The only genuinely lost row in this branch remains dial 3's forLensJsonHeader. WRONG rows on the working-tree population reach ZERO here. test/qackorigincheck.sh's FIXTURE MOVED, and this is the interesting consequence. Its invariant — a zero-magnitude ack must never become a permanent blank check — was driven through api-surface's new-symbol row, which no longer exists. `dead-code` is the other zero-magnitude kind with both origins (born uncalled vs lost its last caller), so all ten checks are re-pointed there, including both halves of the legacy-bare-token migration. The mechanism is unchanged; only the kind that reaches it is. test/qdrefpaircheck.sh's recorded literal moved 18 -> 8 for the 2026-08-15 wave shas, with the reason written beside it: 18 is what the kinds reported before this dial round. dmm for the same pair is still 0.530, which is the cross-check that the corpus did not move — only the tiers did. GATE: test/qddialscheck.sh §4, seven arms, five RED on the pre-change binary (the shrink row, the defaulted row's severity, the doubled `wide` row, the new-export row, and api-new-surface= itself). mcpattrparitycheck, mcpclidiffcheck, jsonparitycheck, attrvocabcheck, legendcoveragecheck, legenddriftcheck, printffmtparitycheck, docscommandscheck, qualnewcheck, qualitypanelcheck, staleackcheck, qualityscopecheck, qdrefpaircheck, qackorigincheck, xmlwellformed: PASS. Co-Authored-By: Claude Fable 5.1 --- src/mcpverbs.h | 5 +- src/quality.h | 138 ++++++++++++++++++++++++++++++++++++++-- src/verbs_quality.h | 17 +++-- test/qackorigincheck.sh | 92 ++++++++++++++------------- test/qddialscheck.sh | 53 +++++++++++++++ test/qdrefpaircheck.sh | 23 +++++-- 6 files changed, 266 insertions(+), 62 deletions(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 59517cebc..9bb74ac22 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -3102,6 +3102,7 @@ struct QualityDeltaOutcome std::size_t ackedByRename = 0; std::size_t ackedByContent = 0; std::size_t registerMacroExcluded = 0; // P2.2: the CLI's disclosed dead-code exemption count — see quality.h + std::size_t apiNewSurface = 0; // Q-DIAL-4: the CLI's api-new-surface= count — see quality.h }; // §B6 M10 — a CORRUPT sidecar used to read as "no sidecar". readBaseline reports a file that yields no header, @@ -3185,7 +3186,7 @@ inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) auto acks = rw::quality::readAckRecords( qualityAcksPath( root ) ); const auto heal = rw::quality::healIdentity( baseSel.snapshot, acks, ing, g, root, root, /*wantContentIds=*/false ); - oc.regs = rw::quality::computeDelta( ing, g, baseSel.snapshot, root, {}, rw::kDefaultMaxFileBytes, &oc.registerMacroExcluded ); + oc.regs = rw::quality::computeDelta( ing, g, baseSel.snapshot, root, {}, rw::kDefaultMaxFileBytes, &oc.registerMacroExcluded, &oc.apiNewSurface ); // signal-to-noise round: honor the per-finding ack ratchet exactly like the CLI — the acks sidecar is // root-qualified (same SIDECAR LOCATION discipline as the baseline), suppression is reported via `acked`. @@ -3261,6 +3262,8 @@ inline std::string qualityDeltaJson( const std::string& root, std::string& errOu // zero, unlike the identity fields just below): mcpclidiffcheck.sh's JSON-key-set lens // diffs this verb against `--quality-delta --json`, and the CLI never omits it either. + ",\"register-macro-excluded\":" + std::to_string( oc.registerMacroExcluded ) + // Q-DIAL-4 — same always-present rule, same mcpclidiffcheck key-set lens. + + ",\"api-new-surface\":" + std::to_string( oc.apiNewSurface ) // R1 IDENTITY — the CLI root's identity disclosure, spelled in JSON. Present only when // git could be read at all, exactly like the CLI arm (absent ≠ zero — see the legend). + oc.identityJson diff --git a/src/quality.h b/src/quality.h index 462d72fa3..404041bf9 100644 --- a/src/quality.h +++ b/src/quality.h @@ -912,6 +912,85 @@ inline std::vector codeLocByNode( const IngestResult& ing ) return out; } +// Q-DIAL-4 (2026-09-10) — DOES THE LAST DECLARED PARAMETER CARRY A DEFAULT? +// +// 113 of the 132 `api-surface` acks in this repo's own committed ledger (85.6%) say the same sentence: "one +// trailing DEFAULTED parameter, every existing caller compiles unchanged". A kind whose acks are 86% one +// shape is describing that shape, so the shape is read off the signature and reported sev="minor" instead of +// being acked one row at a time. It is still a row — the contract DID change, and a defaulted parameter is +// how most contract rot starts. +// +// A BRACE-DEPTH SCAN, NOT A PARSER, and the floor is stated: the signature's first '(' opens the parameter +// list, its matching ')' closes it, the last comma at depth 0 starts the final parameter, and an '=' in that +// final parameter is a default. Depth counts ( ) [ ] { } and, for C++ templates, < > — which is where the +// heuristic can be fooled (`a < b` inside a default expression, an `operator<`), and where being fooled costs +// exactly one severity tier on one row. Languages that spell defaults the same way (Python, TypeScript, PHP, +// Ruby, C#, Swift) are covered by the same scan for free; a language that does not spell them at all simply +// never matches. +inline bool trailingParamHasDefault( std::string_view signature ) noexcept +{ + const std::size_t open = signature.find( '(' ); + if( open == std::string_view::npos ) + { + return false; + } + int depth = 0; + int angle = 0; + std::size_t lastComma = std::string_view::npos; + std::size_t close = std::string_view::npos; + for( std::size_t i = open; i < signature.size(); ++i ) + { + const char c = signature[i]; + if( c == '(' || c == '[' || c == '{' ) { ++depth; } + else if( c == ')' || c == ']' || c == '}' ) + { + --depth; + if( depth == 0 ) { close = i; break; } + } + else if( c == '<' ) { ++angle; } + else if( c == '>' && angle > 0 ) { --angle; } + else if( c == ',' && depth == 1 && angle == 0 ) { lastComma = i; } + } + if( close == std::string_view::npos || close <= open + 1 ) + { + return false; // unclosed, or an empty parameter list + } + const std::size_t from = ( lastComma == std::string_view::npos ) ? open + 1 : lastComma + 1; + const std::string_view last = signature.substr( from, close - from ); + for( std::size_t i = 0; i < last.size(); ++i ) + { + if( last[i] != '=' ) + { + continue; + } + const bool cmp = ( i + 1 < last.size() && last[ i + 1 ] == '=' ) + || ( i > 0 && ( last[ i - 1 ] == '=' || last[ i - 1 ] == '!' || last[ i - 1 ] == '<' || last[ i - 1 ] == '>' ) ); + if( !cmp ) + { + return true; + } + } + return false; +} + +// The per-NODE answer for THIS tree, read off each symbol's own signature bytes in the same one-pass shape +// codeLocByNode uses. forEachSymbolBody hands back [sigStartByte, endByte), and the signature is its prefix. +inline std::vector trailingDefaultByNode( const IngestResult& ing ) +{ + std::vector out( ing.symbols.size(), 0 ); + forEachSymbolBody( ing, [ & ]( NodeId i, const Symbol& s, std::string_view body ) + { + const std::size_t sigLen = s.sigEndByte > s.sigStartByte ? std::size_t( s.sigEndByte - s.sigStartByte ) : 0; + if( sigLen == 0 || sigLen > body.size() ) + { + return; + } + out[i] = trailingParamHasDefault( body.substr( 0, sigLen ) ) ? 1 : 0; + } ); + return out; +} + + // P2.2 — every symbol in THIS tree whose own signature text is a registered-macro call (built ONCE per // computeSnapshot/computeDelta run, exactly like topLevelCallees above), reading each file's bytes once via // forEachSymbolBody — whose per-symbol `body` view already starts at sigStartByte, which is precisely where @@ -5655,13 +5734,18 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap std::string_view root = {}, const std::vector& excludes = {}, std::size_t maxFileBytes = kDefaultMaxFileBytes, - std::size_t* registerMacroExcludedOut = nullptr ) // P2.2: honest disclosure count, additive+optional — see isDeadCandidate + std::size_t* registerMacroExcludedOut = nullptr, // P2.2: honest disclosure count, additive+optional — see isDeadCandidate + std::size_t* apiNewSurfaceOut = nullptr ) // Q-DIAL-4: the api-surface new-symbol COUNT that replaced N never-gating rows { std::vector regs; if( registerMacroExcludedOut ) { *registerMacroExcludedOut = 0; } + if( apiNewSurfaceOut ) + { + *apiNewSurfaceOut = 0; + } // A4-P10 — HOIST the per-symbol quality key. It materializes a path-qualified string + hashes it; the // passes below (4 metric kinds × 2 loops each, dead, api-surface, error-masking, short-horizon-churn) each @@ -6043,6 +6127,17 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap std::uint32_t& slot = nowParamsBySym[ keyByNode[i] ]; slot = std::max( slot, std::uint32_t( ing.symbols[i].params ) ); } + // Q-DIAL-4 — the two inputs the tiering below reads. `paramsRowKeys` is derived from the rows ALREADY + // pushed rather than plumbed out of perSymbolKind: `params` is the only kind that can have reported an + // arity change by now, and reading it off `regs` keeps the fold honest even if that kind's own gate moves. + std::vector paramsRowKeys; + for( const Regression& r : regs ) + { + if( r.kind == "params" ) { paramsRowKeys.push_back( r.key ); } + } + std::sort( paramsRowKeys.begin(), paramsRowKeys.end() ); + const std::vector trailingDefaults = trailingDefaultByNode( ing ); + ScratchMap apiSeen( ing.symbols.size() ); for( NodeId i = 0; i < ing.symbols.size(); ++i ) { @@ -6059,7 +6154,21 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap if( !std::binary_search( base.publicApi.begin(), base.publicApi.end(), key ) ) { const bool isNewSymbol = !existedAtBaseline( key ); // SAME oracle the r26 origin axis uses — one source of truth - regs.push_back( { "api-surface", g.canonId[i], 0, 0, key, isNewSymbol, isNewSymbol ? "new-symbol" : "contract-change", isNewSymbol } ); + if( isNewSymbol ) + { + // Q-DIAL-4 — A COUNT, NOT N ROWS. This row could never gate (the legend says so), it is one + // per new export, and it dominated the document: 103 of 119 api-surface rows over 40 replayed + // commits, 193 of the 1,177 rows in this repo's own committed ack ledger — acked one at a + // time, by hand, for a fact the header can state in one attribute. api-new-surface= on the + // root says how much new public surface arrived; nothing is hidden, and nothing about it was + // ever actionable per row. + if( apiNewSurfaceOut ) + { + ++( *apiNewSurfaceOut ); + } + continue; + } + regs.push_back( { "api-surface", g.canonId[i], 0, 0, key, false, "contract-change", false } ); // a visibility flip: it existed, and it is public now stampLoc( i ); continue; } @@ -6070,11 +6179,30 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap continue; // no baseline params recorded — nothing to compare } const std::uint32_t nowParams = nowParamsBySym[ key ]; // MAX-aggregated — see the overload-trap note above - if( nowParams != pit->second ) + if( nowParams == pit->second ) { - regs.push_back( { "api-surface", g.canonId[i], pit->second, nowParams, key, false, "contract-change", false } ); // origin: reached only for a symbol already in the baseline public set - stampLoc( i ); + continue; + } + if( nowParams < pit->second ) + { + continue; // Q-DIAL-4 — the surface got SMALLER. This document's first sentence is "only what a + // change made WORSE"; three rows over 40 commits reported an arity DROP as a + // regression (probeBodyCost 7->5, selectMonotoneBodySubset 7->5, + // liftPackageDirMention 4->3). Drift is not the contract this verb publishes. + } + if( std::binary_search( paramsRowKeys.begin(), paramsRowKeys.end(), key ) ) + { + continue; // Q-DIAL-4 — ONE FACT, ONE ROW. The `params` kind already reported this symbol's arity + // change, and it is the highest-precision kind in the table (77% TRUE); a second row + // saying the same thing under another kind is what agents ack. Synthetic S3 + // (3 -> 7 parameters) produced two rows for one edit. } + // Q-DIAL-4 — one ADDED parameter that carries a DEFAULT is source-compatible by construction: every + // existing caller still compiles, which is what 113 of this repo's 132 api-surface acks say in those + // words. Still a row (the contract moved), reported sev="minor". + const bool trailingDefault = nowParams == pit->second + 1 && i < trailingDefaults.size() && trailingDefaults[i] != 0; + regs.push_back( { "api-surface", g.canonId[i], pit->second, nowParams, key, trailingDefault, "contract-change", false } ); // origin: reached only for a symbol already in the baseline public set + stampLoc( i ); } // ── §D#4-1 error-masking (GitClear +47%) ────────────────────────────────────────────────────────────── diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 145d25329..0a3ba3961 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -159,6 +159,7 @@ struct DeltaBasis gtl::btree_map acks; rw::quality::IdentityHealing healing; std::size_t registerMacroExcluded = 0; // P2.2: disclosed dead-code exemption count + std::size_t apiNewSurface = 0; // Q-DIAL-4: new PUBLIC symbols this change added — the count that replaced one never-gating row each std::size_t acksBadLines = 0; // 2026-09-06: .ripwire_quality_acks lines skipped as unparseable (disclosed on the root) }; @@ -195,7 +196,7 @@ std::optional resolveDeltaBasis( const MainDispatch& d, const std::string& out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, refs.target().ing, refs.target().g, out.deltaRoot, root, cfg.qualityAck, refs.rangeSpan ); out.regs = quality::computeDelta( refs.target().ing, refs.target().g, out.baseSel.snapshot, - out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded ); + out.deltaRoot, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface ); return std::nullopt; } @@ -254,7 +255,7 @@ std::optional resolveDeltaBasis( const MainDispatch& d, const std::string& out.acks = quality::readAckRecords( quality::acksPath( root ), out.acksBadLines ); out.healing = quality::healIdentity( out.baseSel.snapshot, out.acks, d.ing, d.g, std::string( cfg.rootPath ), root, cfg.qualityAck ); - out.regs = quality::computeDelta( d.ing, d.g, out.baseSel.snapshot, cfg.rootPath, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded ); + out.regs = quality::computeDelta( d.ing, d.g, out.baseSel.snapshot, cfg.rootPath, cfg.excludes, cfg.maxFileBytes, &out.registerMacroExcluded, &out.apiNewSurface ); return std::nullopt; } @@ -485,6 +486,10 @@ inline constexpr const char* kQdLegendCore = "preexisting by construction. preexisting-worse= and new-symbol= partition regressions=. stale= is a " "FOURTH axis, never gating and never counted in regressions=: rows in the .ripwire_quality_acks ledger " "whose target no longer applies. " + "api-new-surface= is a COUNT, not a finding: how many symbols this change adds to the PUBLIC surface. " + "Never gates, never counted in regressions=, printed even at zero. It used to be one row per new export, " + "which the legend itself said could never gate; a count says the same thing without asking a reader to " + "page past it, and nothing narrows what the CONTRACT-CHANGE rows below still report. " "register-macro-excluded= is a FLOOR, not a finding: symbols this run excluded from the dead-code kind " "because their own definition is a registered self-registering test/benchmark macro call. Never gates, " "never counted in regressions=, printed even at zero (zero means none excluded, not that the check did " @@ -1310,9 +1315,9 @@ std::optional runQualityDelta( const MainDispatch& d ) const std::string absorbedJson = baselineAbsorbed == 0 ? std::string() : ",\"baseline_absorbed\":" + std::to_string( baselineAbsorbed ); rw::emitTo( stdout, "{{\"baseline\":\"{}\",\"regressions\":{},\"minor\":{},\"acked\":{},\"stale\":{}," - "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{},\"register-macro-excluded\":{},\"at\":{}{}{}{}{}{},\"r\":[", + "\"preexisting-worse\":{},\"new-symbol\":{},\"gating\":{},\"register-macro-excluded\":{},\"api-new-surface\":{},\"at\":{}{}{}{}{}{},\"r\":[", jsonStr( baseMarkerJ ).c_str(), regs.size(), minorCount, ackedCount, staleAcks.size(), - preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, atJsonJ.c_str(), refs.jsonAttrs.c_str(), + preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, atJsonJ.c_str(), refs.jsonAttrs.c_str(), identityJson.c_str(), scopeJson.c_str(), configWarnJson.c_str(), absorbedJson.c_str() ); // P1: one row emitter, called for both halves of the scope partition — the disclosed rows carry // the identical key set, so nothing about a row changes by being someone else's. `gatingAllowed` @@ -1416,8 +1421,8 @@ std::optional runQualityDelta( const MainDispatch& d ) if( baseSel.sidecarBadLines > 0 ) { sidecarHealthAttrs += " baseline_bad_lines=\"" + std::to_string( baseSel.sidecarBadLines ) + "\""; } if( basis.acksBadLines > 0 ) { sidecarHealthAttrs += " acks_bad_lines=\"" + std::to_string( basis.acksBadLines ) + "\""; } // at= anchors this regression list to the commit (+dirty state) it was computed against. - rw::emitTo( stdout, "", - baseMarker, regs.size(), minorCount, ackedCount, staleAcks.size(), preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, + rw::emitTo( stdout, "", + baseMarker, regs.size(), minorCount, ackedCount, staleAcks.size(), preexistingCount, newSymbolCount, gatingCount, basis.registerMacroExcluded, basis.apiNewSurface, // R-I: at= is OMITTED for the ref-pair form rather than stamped with the working tree's // sha, which would anchor the list to a commit it was not computed from. base_ref= and // target_ref= are the anchor there, and they carry FULL shas because a wave measurement diff --git a/test/qackorigincheck.sh b/test/qackorigincheck.sh index 86d081c72..5af2ff48b 100755 --- a/test/qackorigincheck.sh +++ b/test/qackorigincheck.sh @@ -1,10 +1,16 @@ #!/usr/bin/env bash # qackorigincheck.sh — r27 P0.3 gate: a ZERO-MAGNITUDE ack must never become a permanent blank check. # -# THE BUG THIS PINS. `applyAckRatchet` suppresses a finding when `now <= ackNow`. The api-surface tier-A push -# emits was=now=0 for BOTH shapes it can produce: -# * origin="new-symbol" — additive surface on brand-new code: sev=minor, NEVER gates; -# * surface="contract-change" — a symbol that already existed became part of the public contract: major, GATES. +# THE BUG THIS PINS. `applyAckRatchet` suppresses a finding when `now <= ackNow`. A zero-magnitude kind emits +# was=now=0 for BOTH shapes it can produce: +# * origin="new-symbol" — the finding exists only because the code is new: NEVER gates; +# * (no origin attribute) — preexisting-worse: something that already existed got worse. GATES. +# +# THE FIXTURE MOVED, 2026-09-10 (Q-DIAL-4). It used to drive this through `api-surface`, whose tier-A push +# emitted one row per new export; that row can never gate, so it is a header COUNT (api-new-surface=) now and +# the kind no longer produces a new-symbol row at all. `dead-code` is the other zero-magnitude kind with both +# origins — born uncalled vs lost its last caller — and it drives the identical mechanism, so the invariant is +# pinned there instead of being retired with the fixture that happened to reach it first. # Both landed under the SAME (kind, key) ack identity, so `--quality-ack` sweeping up the harmless new-symbol # rows (209 of this repo's own 402 committed ack lines were exactly that) meant the later, genuine # private -> public flip on the same symbol hit `0 <= 0` and was suppressed FOREVER. `dead-code` (always now=0) @@ -18,19 +24,19 @@ # that is what those rows overwhelmingly were, and the rows we cannot distinguish are re-surfaced rather than # silently kept, i.e. fail-closed. # -# Fixture mechanics. Tier-A "contract-change" needs a canonId that EXISTS in the baseline's per-symbol maps but -# is ABSENT from its public set. That is exactly the shape of a baseline written by an older binary with a -# narrower notion of "public", and it is reproduced deterministically here by stripping the `api ` lines out of -# a freshly-written .ripwire_quality_baseline sidecar. Nothing about the fix depends on the fixture's route to -# that state — only on the two rows sharing an identity, which they do. +# Fixture mechanics. The PREEXISTING shape needs a canonId that EXISTS in the baseline's per-symbol maps but is +# ABSENT from its dead set — exactly the shape of a baseline written before the symbol lost its last caller, +# and reproduced deterministically here by stripping the `dead ` lines out of a freshly-written +# .ripwire_quality_baseline sidecar. Nothing about the fix depends on the fixture's route to that state — only +# on the two rows sharing an identity, which they do. # # Checks: -# (a) phase 1 — a new public symbol yields origin="new-symbol", sev=minor, and does NOT gate (exit 0). -# (b) --quality-ack records it under an ORIGIN-QUALIFIED token (`api-surface:new-symbol`), not a bare kind. +# (a) phase 1 — a symbol born uncalled yields origin="new-symbol" and does NOT gate (exit 0). +# (b) --quality-ack records it under an ORIGIN-QUALIFIED token (`dead-code:new-symbol`), not a bare kind. # (c) the ack still suppresses its OWN row on a re-run (the ratchet still works for the class it accepted). -# (d) THE FIX — with that ack in place, the SAME symbol's contract-change row is still reported and GATES +# (d) THE FIX — with that ack in place, the SAME symbol's PREEXISTING row is still reported and GATES # (exit 2). Pre-fix this row was suppressed and the run exited 0. -# (e) MIGRATION — a hand-written LEGACY bare `ack api-surface 0` line suppresses the new-symbol row +# (e) MIGRATION — a hand-written LEGACY bare `ack dead-code 0` line suppresses the new-symbol row # (it is read as the :new-symbol variant) but does NOT suppress the contract-change row. # (f) a magnitude-bearing ack is untouched: its token stays bare and the ratchet still re-reports on worsening. # @@ -68,73 +74,73 @@ EOF git -C "$REPO" init -q; git -C "$REPO" config user.email x@y; git -C "$REPO" config user.name x git -C "$REPO" add -A; git -C "$REPO" commit -qm init -# ── (a) phase 1: a brand-new public symbol → origin="new-symbol", minor, does not gate ──────────────────── -cat >> "$REPO/inc/api.h" <<'EOF' -int freshExport( int a ); +# ── (a) phase 1: a brand-new symbol born uncalled → origin="new-symbol", does not gate ──────────────────── +cat >> "$REPO/src/lib.cpp" <<'EOF' +int freshOrphan( int a ) { return a * 3; } EOF run --quality-delta >"$TMP/p1" 2>/dev/null; rc1=$? -{ [ "$rc1" -eq 0 ] && grep -q 'sym="[^"]*freshExport"[^/]*origin="new-symbol"' "$TMP/p1"; } \ - && ok "new public symbol reports origin=\"new-symbol\" and does not gate (exit 0)" \ - || { no "phase 1 unexpected (exit=$rc1)"; tr '<' '\n' < "$TMP/p1" | grep freshExport; } +{ [ "$rc1" -eq 0 ] && tr '<' '\n' < "$TMP/p1" | grep 'freshOrphan' | grep -q 'origin="new-symbol"'; } \ + && ok "a symbol born uncalled reports origin=\"new-symbol\" and does not gate (exit 0)" \ + || { no "phase 1 unexpected (exit=$rc1)"; tr '<' '\n' < "$TMP/p1" | grep freshOrphan; } # ── (b) --quality-ack writes an ORIGIN-QUALIFIED token ──────────────────────────────────────────────────── run --quality-delta --quality-ack="fixture: additive surface" >/dev/null 2>&1 -if grep -q '^ack api-surface:new-symbol ' "$ACKS" 2>/dev/null; then - ok "--quality-ack records the zero-magnitude row as 'api-surface:new-symbol' (origin-qualified identity)" +if grep -q '^ack dead-code:new-symbol ' "$ACKS" 2>/dev/null; then + ok "--quality-ack records the zero-magnitude row as 'dead-code:new-symbol' (origin-qualified identity)" else - no "ack file has no origin-qualified api-surface token — zero-magnitude acks still key on the bare kind" + no "ack file has no origin-qualified dead-code token — zero-magnitude acks still key on the bare kind" cat "$ACKS" 2>/dev/null | head -5 fi -AKEY="$( sed -n 's/^ack api-surface:new-symbol \([0-9a-f]*\) .*/\1/p' "$ACKS" | head -1 )" +AKEY="$( sed -n 's/^ack dead-code:new-symbol \([0-9a-f]*\) .*/\1/p' "$ACKS" | head -1 )" [ -n "$AKEY" ] && ok "recovered the acked identity key ($AKEY) for the cross-origin check" \ || no "could not recover the acked identity key — later checks are vacuous" # ── (c) the ack still suppresses its own row ────────────────────────────────────────────────────────────── ACKED_FILE="$TMP/acks_qualified"; cp "$ACKS" "$ACKED_FILE" run --quality-delta >"$TMP/p1b" 2>/dev/null; rc1b=$? -{ [ "$rc1b" -eq 0 ] && ! grep -q 'freshExport' "$TMP/p1b" && grep -q 'acked="[1-9]' "$TMP/p1b"; } \ +{ [ "$rc1b" -eq 0 ] && ! grep -q 'freshOrphan' "$TMP/p1b" && grep -q 'acked="[1-9]' "$TMP/p1b"; } \ && ok "the ack still suppresses its OWN new-symbol row, honestly (acked=N)" \ - || { no "the ack no longer suppresses the row it was taken against"; tr '<' '\n' < "$TMP/p1b" | grep -E 'quality-delta |freshExport'; } + || { no "the ack no longer suppresses the row it was taken against"; tr '<' '\n' < "$TMP/p1b" | grep -E 'quality-delta |freshOrphan'; } # ── (e1) MIGRATION, keep half: a LEGACY bare zero-magnitude ack still suppresses the class it was recorded # for. Checked HERE, while the tree is still in the phase-1 (new-symbol) shape. if [ -n "$AKEY" ]; then - printf '# legacy pre-r27 ack file\nack api-surface %s 0 legacy bare token\n' "$AKEY" > "$ACKS" + printf '# legacy pre-r27 ack file\nack dead-code %s 0 legacy bare token\n' "$AKEY" > "$ACKS" run --quality-delta >"$TMP/p1c" 2>/dev/null - grep -q 'freshExport' "$TMP/p1c" \ - && { no "legacy bare ack stopped suppressing the new-symbol row it was recorded for"; tr '<' '\n' < "$TMP/p1c" | grep freshExport | head -2; } \ + grep -q 'freshOrphan' "$TMP/p1c" \ + && { no "legacy bare ack stopped suppressing the new-symbol row it was recorded for"; tr '<' '\n' < "$TMP/p1c" | grep freshOrphan | head -2; } \ || ok "a LEGACY bare ack still suppresses the new-symbol row it was recorded for (migration preserves meaning)" fi cp "$ACKED_FILE" "$ACKS" -# ── (d) THE FIX: the same symbol's CONTRACT-CHANGE row is not blank-checked by that ack ──────────────────── -# Commit the header (so freshExport exists at the baseline), pin a sidecar baseline, then strip its `api ` -# records — freshExport is now present in the per-symbol maps but absent from the public set, which is the -# tier-A contract-change shape. Same canonId ⇒ same identity key as the ack taken in (b). -git -C "$REPO" add -A; git -C "$REPO" commit -qm "export freshExport" >/dev/null +# ── (d) THE FIX: the same symbol's PREEXISTING row is not blank-checked by that ack ──────────────────────── +# Commit (so freshOrphan exists at the baseline), pin a sidecar baseline, then strip its `dead ` records — +# freshOrphan is now present in the per-symbol maps but absent from the dead set, which is the preexisting +# shape. Same canonId ⇒ same identity key as the ack taken in (b). +git -C "$REPO" add -A; git -C "$REPO" commit -qm "add freshOrphan" >/dev/null run --quality-baseline >/dev/null 2>&1 [ -s "$BASE" ] && ok "pinned a baseline sidecar for the contract-change phase" || no "no baseline sidecar written" -grep -v '^api ' "$BASE" > "$TMP/base_noapi" && cp "$TMP/base_noapi" "$BASE" +grep -v '^dead ' "$BASE" > "$TMP/base_nodead" && cp "$TMP/base_nodead" "$BASE" run --quality-delta >"$TMP/p2" 2>/dev/null; rc2=$? -CCROW="$( tr '<' '\n' < "$TMP/p2" | grep 'freshExport' | grep 'contract-change' )" +CCROW="$( tr '<' '\n' < "$TMP/p2" | grep 'freshOrphan' | grep 'kind="dead-code"' | grep -v 'origin="new-symbol"' )" if [ -n "$CCROW" ] && [ "$rc2" -eq 2 ]; then - ok "the SAME symbol's contract-change row survives the new-symbol ack and GATES (exit 2)" + ok "the SAME symbol's PREEXISTING row survives the new-symbol ack and GATES (exit 2)" else - no "contract-change row suppressed or non-gating (exit=$rc2) — the zero-magnitude blank check is back" - tr '<' '\n' < "$TMP/p2" | grep -E 'quality-delta |freshExport' | head -4 + no "preexisting row suppressed or non-gating (exit=$rc2) — the zero-magnitude blank check is back" + tr '<' '\n' < "$TMP/p2" | grep -E 'quality-delta |freshOrphan' | head -4 fi printf '%s' "$CCROW" | grep -q 'gating="1"' \ - && ok "the surviving contract-change row is marked gating=\"1\"" \ + && ok "the surviving preexisting row is marked gating=\"1\"" \ || no "the gating row carries no gating=\"1\" marker" # ── (e2) MIGRATION, fail-closed half: the same LEGACY line must NOT reach the contract-change row ───────── if [ -n "$AKEY" ]; then - printf '# legacy pre-r27 ack file\nack api-surface %s 0 legacy bare token\n' "$AKEY" > "$ACKS" + printf '# legacy pre-r27 ack file\nack dead-code %s 0 legacy bare token\n' "$AKEY" > "$ACKS" run --quality-delta >"$TMP/p3" 2>/dev/null; rc3=$? - { [ "$rc3" -eq 2 ] && tr '<' '\n' < "$TMP/p3" | grep 'freshExport' | grep -q 'contract-change'; } \ - && ok "a LEGACY bare zero-magnitude ack does NOT suppress the contract-change row (fail-closed migration)" \ - || { no "legacy bare ack still blank-checks the contract-change row (exit=$rc3)"; tr '<' '\n' < "$TMP/p3" | grep freshExport | head -3; } + { [ "$rc3" -eq 2 ] && tr '<' '\n' < "$TMP/p3" | grep 'freshOrphan' | grep 'kind="dead-code"' | grep -qv 'origin="new-symbol"'; } \ + && ok "a LEGACY bare zero-magnitude ack does NOT suppress the preexisting row (fail-closed migration)" \ + || { no "legacy bare ack still blank-checks the preexisting row (exit=$rc3)"; tr '<' '\n' < "$TMP/p3" | grep freshOrphan | head -3; } fi # ── (f) magnitude-bearing acks are untouched (bare token, ratchet still re-reports on worsening) ────────── diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 96375416c..60f4b783d 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -201,5 +201,58 @@ vrow complexity cxCrosser | grep -q 'bar="15"' && ok "complexity: bar=15 unchang && ok "verbosity/complexity: byte-identical run to run (deterministic)" || no "verbosity/complexity: non-deterministic delta" +# ── 4) api-surface: a count for new exports, no row for a SMALLER surface, one row per fact ────────────── +# 103 of 119 api-surface rows over 40 replayed commits carried origin="new-symbol", which the legend itself +# says can never gate — and 193 of the 1,177 rows in this repo's committed ack ledger are that shape, acked +# by hand one at a time. Three more findings from the same replay: three rows reported an arity DROP as a +# regression in a document whose first sentence is "only what a change made WORSE"; 113 of 132 api-surface +# acks say "one trailing DEFAULTED parameter, every existing caller compiles unchanged"; and one parameter +# change emitted TWO rows, under `params` and again under `api-surface`. +AP="$WORK/api"; mkdir -p "$AP/src" +( cd "$AP" && git init -q && git config user.email t@t && git config user.name t && git config commit.gpgsign false ) +apigen(){ python3 - "$AP/src" "$1" <<'PY' +import sys, os +d, stage = sys.argv[1], sys.argv[2] +after = stage == "after" +h = [] +h.append("inline int shrink( int a, int b%s ){ return a + b%s; }\n" % ("" if after else ", int c", "" if after else " + c")) +h.append("inline int defaulted( int a%s ){ return a%s; }\n" % (", int b = 0" if after else "", " + b" if after else "")) +h.append("inline int wide( int a, int b, int c, int d, int e%s ){ return a+b+c+d+e%s; }\n" + % (", int f, int g" if after else "", "+f+g" if after else "")) +if after: + h.append("inline int fresh( int a ){ return a + 1; }\n") +open(os.path.join(d, "api.hpp"), "w").write("".join(h)) +m = ['#include "api.hpp"\n', "int driver(){\n", + " return shrink( 1, 2%s ) + defaulted( 3 ) + wide( 1,2,3,4,5%s )%s;\n" % ("" if after else ", 3", "" if after else "", " + fresh( 9 )" if after else ""), + "}\n", "int main(){ return driver(); }\n"] +open(os.path.join(d, "m.cpp"), "w").write("".join(m)) +PY +} +apigen before +( cd "$AP" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) +apigen after +OAP="$( cd "$AP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +row "$OAP" api-surface shrink >/dev/null \ + && { no "api-surface: shrink 3 -> 2 params reported — a SMALLER surface is not what a change made worse"; rows "$OAP"; } \ + || ok "api-surface: an arity DROP produces no row" +row "$OAP" api-surface defaulted | grep -q 'sev="minor"' \ + && ok "api-surface: one trailing DEFAULTED parameter is sev=minor (callers still compile)" \ + || { no "api-surface: a trailing defaulted parameter should be minor"; rows "$OAP"; } +APIWIDE="$( rows "$OAP" | grep -c 'sym="wide"' )" +[ "$APIWIDE" = 1 ] && ok "api-surface: wide 5 -> 7 params emits ONE row, not one per kind" \ + || { no "api-surface: expected 1 row for wide, got $APIWIDE (params + api-surface both fired)"; rows "$OAP" | grep 'sym="wide"'; } +rows "$OAP" | grep -q 'kind="params" sym="wide"' \ + && ok "api-surface: the surviving row is the params one (77% precision, the kind that keeps the fact)" \ + || { no "api-surface: the params row must be the one that survives"; rows "$OAP" | grep 'sym="wide"'; } +row "$OAP" api-surface fresh >/dev/null \ + && { no "api-surface: a brand-new export is still a row — it can never gate, so it is a count"; rows "$OAP"; } \ + || ok "api-surface: a brand-new export produces no row" +printf '%s' "$OAP" | grep -q 'api-new-surface="1"' \ + && ok "api-surface: the root carries api-new-surface=\"1\" (nothing is hidden, it is counted)" \ + || { no "api-surface: api-new-surface= missing or wrong on the root"; printf '%s' "$OAP" | head -c 200; } +[ "$OAP" = "$( cd "$AP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "api-surface: byte-identical run to run (deterministic)" || no "api-surface: non-deterministic delta" + + [ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" exit "$fail" diff --git a/test/qdrefpaircheck.sh b/test/qdrefpaircheck.sh index 6b153b562..d4b2f74b8 100755 --- a/test/qdrefpaircheck.sh +++ b/test/qdrefpaircheck.sh @@ -18,11 +18,20 @@ # # Two literals ARE pinned, and only as a cross-check that the two shas still name the round the comment # above describes: the harvest round record (PLAN_HARVEST_REPORTS_2026-08-15/ROUTING_LEDGER.md) states -# `--dmm=4b9386c..ba380b5` = 0.530 and 18 gating rows. Both reproduce. +# `--dmm=4b9386c..ba380b5` = 0.530 and 18 gating rows. # -# ── THE ONE DEFENSIBLE DISCREPANCY: 18 vs 11 ───────────────────────────────────────────────────────────── -# The overlay reports 18 gating rows; the ref-pair form reports 11. The difference is exactly the 7 -# short-horizon-churn rows, and it is a property of the QUESTION, not a bug: +# THE 18 IS A HISTORICAL READING, AND IT MOVED — 2026-09-10, the per-kind dial round (test/qddialscheck.sh). +# 18 is what the kinds reported when churn="self" gated on its own, when verbosity counted physical lines, +# when any growth over the bar was major, and when every new export was a row. Four of those changed on +# purpose, so the same two shas now report 8. The literal is re-pinned to 8 rather than deleted, because what +# it checks is unchanged: that these shas still name a wave with regressions in it. dmm is a different +# instrument and does not read the gating tiers, so 0.530 is untouched — which is itself the cross-check that +# the CORPUS did not move, only the tiers. +# +# ── THE ONE DEFENSIBLE DISCREPANCY: the overlay's total exceeds the ref-pair form's ─────────────────────── +# The overlay's gating total is higher than the ref-pair form's, and the difference is exactly the +# short-horizon-churn rows (7 of the historical 18; the dial round left fewer). It is a property of the +# QUESTION, not a bug: # # The churn kind needs git history AT THE TREE BEING JUDGED — it counts commits per file in a recent # window and compares body hashes against a window-reference commit. The overlay's judged tree is a real @@ -217,9 +226,9 @@ else # the two RECORDED literals from the round record — a cross-check that these shas still name that wave overlayTotal=$(( oracleN + overlayChurn )) - [ "$overlayTotal" = 18 ] \ - && ok "(E) the overlay reproduces the RECORDED 18 gating rows (= $oracleN + $overlayChurn churn)" \ - || no "(E) the overlay gave $overlayTotal gating rows; the round record states 18 — the shas or the corpus moved" + [ "$overlayTotal" = 8 ] \ + && ok "(E) the overlay reproduces the pinned 8 gating rows (= $oracleN + $overlayChurn churn; 18 pre-dial)" \ + || no "(E) the overlay gave $overlayTotal gating rows; this binary is pinned at 8 (18 before the 2026-09-10 dial round) — the shas, the corpus or a kind's tier moved" dmmVal="$( "$BIN" "$ROOT" "--dmm=$WAVE_A..$WAVE_B" 2>/dev/null | grep -o ' dmm="[0-9.]*"' | head -1 | sed -E 's/.*"([0-9.]*)".*/\1/' )" # tolerance band, not equality: dmm is a float printed to 3 places (house float rule). if [ -n "$dmmVal" ] && awk -v v="$dmmVal" 'BEGIN{ exit !(v > 0.525 && v < 0.535) }'; then From da7af625543881aff54d4aedd363603f93288480 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:48:28 -0400 Subject: [PATCH 18/73] fix(mention,situ): a disclosure that names no total, and one that could never say yes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THREE FLAGS THAT SAY SOMETHING WAS WITHHELD AND NOTHING ELSE. 1-2. mention_files_capped= and doc_mentions_capped= were noteCap( …, nullptr, … ) — bare booleans, in a file whose sibling caps mention_tokens_capped= and mention_syms_capped= have always carried a total. docs/METHODOLOGY.md §9-3 makes a cut terminal only when the caller can finish in one more KNOWN call; "an indexing cap dropped content not shown here" with no count is not that. Both now carry one: mention_files_total= every DISTINCT file the task's mentions name, kept and refused together, so total - lifted is what kMentionMaxFiles withheld. Computed only when the list is full, so the common anchored query pays the same one size test it always did. namesFileNotKept is now the predicate over mentionUnkeptFiles' output rather than a separate walk — verdict-equivalent by construction, same resolution order, same three rules, a level that resolves on KEPT matches alone still appends nothing. doc_mentions_total= lifted + refused: how many docs the caps had to choose from, beside the doc_mentions= the root already carries. The refused set is now collected instead of broken out of at the first hit — a bare "something was cut" can stop early, a total cannot — and deduped, because one doc under two anchors is one refusal. Nothing in that path touches lensRank, so the lift is unchanged. 3. --test-gate's tests_capped= was the string literal "0" in BOTH dialects (src/situ.h:1096, :1166): a disclosure that could never become "1", so a row cap added later would keep saying nothing was cut while something was. It is now shown_tests < tests, and shown_tests= is the rows the document ACTUALLY emits (r.testRows + shell-gate obligations) rather than a count asserted beside them. It stays PRESENT at 0 rather than being omitted: pageview.h rule 1 pairs shown_*/*_capped per listing, and its sibling untested_capped="0" is pinned by testgatepagecheck (a')/(a-json') and impactpartitioncheck — dropping one half of a documented pair would be a new inconsistency, not a fix for this one. A/B, this branch's binary vs the pre-change build, 11 invocations over two corpora (this tree and the go corpus): 8 BYTE-IDENTICAL, 3 differ ONLY by the added total, 0 unexplained. --for="…src/lexical.h chooseForRanker…" +46 B doc_mentions_total="5" --for="wire src/mcp.h, src/mcpverbs.h, …" +48 B mention_files_total="5" --for="pagerank power iteration" +46 B doc_mentions_total="5" --for="incremental cache invalidation…", --pack-task, --top-k=100000 (both corpora), --test-gate and --test-gate --json byte-identical The attribute rides both the root and the [cut: …] note, which is why the cost is ~46 B and not ~23. est_tokens moves with it, honestly. GATES. mentioncapcheck arm (H): H1/H3 require the total beside each flag AND require it to exceed the shown count; H2 requires it ABSENT when the cap did not fire, so an uncut answer stays byte-identical. Red-proven against the pre-change binary: FAIL H1 the file cut disclosed no usable total (total='' lifted='4') FAIL H3 the doc cut disclosed no usable total (total='' shown='4') testgatepagecheck arm (d): its own fixture (2 test files, 3 symbols) because $R has no test file at all and an arm whose every number is 0 cannot tell a derivation from a literal; shown_tests= is checked against the rows COUNTED in the document, and the JSON dialect against the XML key-for-key. Control on a synthetic COPY, because a literal "0" satisfies the arm on every tree where nothing is cut — which is every tree today: rewrite tests="2" to tests="99" and require the check to reject the contradiction. PASS (d) control: a document claiming tests_capped="0" with shown_tests=2 of 99 is REJECTED NOT LANDED, and it is the other half of C2 F16: the flags still fire when a refused doc or file was never going to reach the answer — C2 measured the doc caps adding ZERO rows at 1x/4x/16x budget while doc_mentions_capped="1" and the legend's "an indexing cap dropped content not shown here" both fired. "Would have ENTERED the answer" is not knowable where the fact is computed: applyDocMentionBoost runs before the ranker and the payload budget decide what is emitted, and absorbCapDisclosure takes the verdict at that same point. Making it knowable means carrying the refused set to all four emit surfaces (--for, --pack-task, and the two MCP verbs) and re-proving byte identity on each — a lane, not a patch. Recorded here rather than approximated, because a disclosure that guesses is the defect twice. Green: mentioncapcheck, testgatepagecheck, printffmtparitycheck (42 verbs), jsoncheck, testgatelegendbudgetcheck, legendcoveragecheck, impactpartitioncheck, mcpclidiffcheck. --- docs/LIMITS.md | 6 +-- src/mention.h | 106 +++++++++++++++++++++++++++++--------- src/situ.h | 18 +++++-- test/mentioncapcheck.sh | 44 ++++++++++++++++ test/testgatepagecheck.sh | 35 +++++++++++++ 5 files changed, 179 insertions(+), 30 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index dc3a655eb..8e55c1330 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -539,9 +539,9 @@ Discloses: `doc_mentions_capped`, `mention_files_capped`, `mention_syms_capped`, | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kDocMentionMaxAnchors` | `8` | 759 | INDEXING | consult only the current top-N anchors | -| `kDocMentionMaxDocsPerAnchor` | `2` | 760 | INDEXING | strongest-anchor-first, capped per anchor | -| `kDocMentionMaxDocsTotal` | `6` | 761 | INDEXING | global cap — bounds token cost regardless of fan-out | +| `kDocMentionMaxAnchors` | `8` | 807 | INDEXING | consult only the current top-N anchors | +| `kDocMentionMaxDocsPerAnchor` | `2` | 808 | INDEXING | strongest-anchor-first, capped per anchor | +| `kDocMentionMaxDocsTotal` | `6` | 809 | INDEXING | global cap — bounds token cost regardless of fan-out | | `kMentionMaxDirectSymbols` | `8` | 160 | INDEXING | directly-named (Scope.name / `name`) symbols, id asc | | `kMentionMaxFiles` | `4` | 158 | INDEXING | strongest evidence only: files named first in the text | | `kMentionMaxRawTokens` | `16` | 157 | INDEXING | extraction cap: first N candidate mention tokens, text order | diff --git a/src/mention.h b/src/mention.h index 7f4a71add..ab9c6abe2 100644 --- a/src/mention.h +++ b/src/mention.h @@ -407,13 +407,21 @@ inline bool namesUnkeptPackageIndex( const IngestResult& ing, const RawMention& return false; } -inline bool namesFileNotKept( const IngestResult& ing, const RawMention& m, const std::vector& kept ) +// WHICH files this mention names that `kept` does not — appended, never cleared, so a caller can union +// across mentions. This is namesFileNotKept's body with "return true on the first one" replaced by "collect +// them all": a bare boolean told the caller something was withheld and neither how much nor how to get it, +// which is §9-3 of docs/METHODOLOGY.md unmet, and the sibling caps in this same file already pass a total. +// Verdict-equivalent to the predicate below by construction — same resolution order, same three rules, and +// a level that resolves with only KEPT matches still ends resolution with nothing appended. +inline void mentionUnkeptFiles( const IngestResult& ing, const RawMention& m, const std::vector& kept, + std::vector& out ) { const std::size_t fileCount = ing.files.size(); for( std::size_t suffixLen = m.segments.size(); suffixLen >= 1; --suffixLen ) { const std::vector suffix( m.segments.end() - suffixLen, m.segments.end() ); - bool named = false; + const std::size_t before = out.size(); + bool named = false; for( std::uint32_t f = 0; f < fileCount; ++f ) { if( !pathSuffixMatches( ing.files[f], suffix ) ) @@ -422,20 +430,35 @@ inline bool namesFileNotKept( const IngestResult& ing, const RawMention& m, cons } if( std::find( kept.begin(), kept.end(), f ) == kept.end() ) { - return true; + out.push_back( f ); + continue; } named = true; } - if( named ) + if( out.size() > before || named ) { - return false; + return; // the longest matching suffix wins and ends resolution — capped or not } } if( !m.isPath && m.segments.size() == 2 && definesScopeName( ing, m.segments[0], m.segments[1] ) ) { - return false; + return; + } + for( std::uint32_t f = 0; f < fileCount; ++f ) + { + if( isIndexBaseName( baseNameOf( ing.files[f] ) ) && dirSuffixMatches( ing.files[f], m.segments ) + && std::find( kept.begin(), kept.end(), f ) == kept.end() ) + { + out.push_back( f ); + } } - return namesUnkeptPackageIndex( ing, m, kept ); +} + +inline bool namesFileNotKept( const IngestResult& ing, const RawMention& m, const std::vector& kept ) +{ + std::vector unkept; + mentionUnkeptFiles( ing, m, kept, unkept ); + return !unkept.empty(); } // The file-cap verdict for the whole task: did kMentionMaxFiles keep out a file ANY mention names? A cut needs a full @@ -450,6 +473,27 @@ inline bool mentionFilesCut( const IngestResult& ing, const std::vector kept.size()` is exactly mentionFilesCut's verdict +// and the two can never disagree. The union is only computed on the runs where the list is full, so the +// common anchored query pays the same one size test it always did. +inline std::uint32_t mentionFilesNamedTotal( const IngestResult& ing, const std::vector& raw, + const std::vector& kept ) +{ + if( kept.size() < kMentionMaxFiles ) + { + return std::uint32_t( kept.size() ); + } + std::vector named( kept.begin(), kept.end() ); + for( const RawMention& m : raw ) + { + mentionUnkeptFiles( ing, m, kept, named ); + } + std::sort( named.begin(), named.end() ); + named.erase( std::unique( named.begin(), named.end() ), named.end() ); + return std::uint32_t( named.size() ); +} + // extract candidate mentions from the task text: '/'-joined path tokens, dot-joined identifier chains, // and `backticked` identifiers. Plain prose words never qualify — precision over recall by design. // `outQualified` counts EVERY token that would have become a mention, window or no window — the scan runs @@ -657,7 +701,11 @@ inline bool applyMentionBoost( const IngestResult& ing, std::string_view task, s liftPackageDirMention( ing, m, mentionedFiles ); } } - noteCap( outInfo, "mention_files_capped", nullptr, mentionFilesCut( ing, raw, mentionedFiles ), 0 ); // a STOP is not a CUT + // a STOP is not a CUT (namesFileNotKept), and a CUT without a total is a fact the caller cannot act on: + // mention_files_total= is every distinct file the task names, so `total - shown` is what the cap withheld. + const std::uint32_t mentionFilesTotal = mentionFilesNamedTotal( ing, raw, mentionedFiles ); + noteCap( outInfo, "mention_files_capped", "mention_files_total", + mentionFilesTotal > mentionedFiles.size(), mentionFilesTotal ); noteCap( outInfo, "mention_syms_capped", "mention_syms_total", directSymbolTotal > kMentionMaxDirectSymbols, directSymbolTotal ); if( mentionedFiles.empty() && directSymbols.empty() ) { @@ -773,12 +821,17 @@ struct DocMentionBoostInfo CapDisclosure caps; }; -// Would any anchor in order[from, to) have lifted a doc that is not already at or above its own lift -// target? True proves kDocMentionMaxDocsTotal turned a liftable doc away when it ended the consult loop; -// false is a proof of the negative, not a shrug. Bounded by kDocMentionMaxAnchors, so it never scans the -// corpus. Callers pass only the CONSULT WINDOW: anchors past it are the kDocMentionMaxAnchors cut, which -// is on the caller's screen and by design carries no attribute. -inline bool docLiftWasRefused( const Graph& g, const std::vector& lensRank, const std::vector& order, std::size_t from, std::size_t to ) +// WHICH docs the anchors in order[from, to) would have lifted and did not — appended to `out`. A doc that +// WAS lifted now sits at its target, so it cannot appear here; every doc that does is one a cap turned away. +// Non-empty proves kDocMentionMaxDocsTotal ended the consult loop on a liftable doc; empty is a proof of +// the negative, not a shrug. Bounded by kDocMentionMaxAnchors, so it never scans the corpus. Callers pass +// only the CONSULT WINDOW: anchors past it are the kDocMentionMaxAnchors cut, which is on the caller's +// screen and by design carries no attribute. +// +// It collects rather than returning bool because the disclosure needs a COUNT: lifted + refused is exactly +// how many docs the caps had to choose from, which is what doc_mentions_total= reports. +inline void collectRefusedDocLifts( const Graph& g, const std::vector& lensRank, const std::vector& order, + std::size_t from, std::size_t to, std::vector& out ) { for( std::size_t k = from; k < to; ++k ) { @@ -792,11 +845,10 @@ inline bool docLiftWasRefused( const Graph& g, const std::vector& lensRan { if( doc < lensRank.size() && lensRank[doc] < target ) { - return true; + out.push_back( doc ); } } } - return false; } inline bool applyDocMentionBoost( const Graph& g, std::vector& lensRank, DocMentionBoostInfo* outInfo = nullptr ) @@ -822,7 +874,7 @@ inline bool applyDocMentionBoost( const Graph& g, std::vector& lensRank, // Set ONLY where a refusal is provable — a doc below its anchor's lift target that a cap turned away. // "There might be more" is not a fact and never sets it. - bool docsCapped = false; + std::vector refusedDocs; // docs a cap turned away — the count half of the disclosure std::size_t stoppedAt = 0; // how far the consult loop actually got — the post-loop sweep resumes here std::uint32_t liftedDocs = 0, usedAnchors = 0; for( std::size_t k = 0; k < topN && liftedDocs < kDocMentionMaxDocsTotal; ++k ) @@ -844,11 +896,12 @@ inline bool applyDocMentionBoost( const Graph& g, std::vector& lensRank, { if( perAnchor >= kDocMentionMaxDocsPerAnchor || liftedDocs >= kDocMentionMaxDocsTotal ) { - // A cap, not the fan-out, ended this anchor: look only until one refused doc is shown liftable. + // A cap, not the fan-out, ended this anchor. The whole remaining fan-out is walked rather + // than broken out of at the first hit: a bare "something was cut" could stop early, a TOTAL + // cannot. Nothing here touches lensRank, so the lift is byte-identical either way. if( doc < lensRank.size() && lensRank[doc] < target ) { - docsCapped = true; - break; + refusedDocs.push_back( doc ); } continue; } @@ -870,9 +923,16 @@ inline bool applyDocMentionBoost( const Graph& g, std::vector& lensRank, } // The other half of the total cap: it can also end the OUTER loop, leaving consulted-window anchors - // whose docs were never looked at (see docLiftWasRefused). - docsCapped = docsCapped || docLiftWasRefused( g, lensRank, order, stoppedAt, topN ); - noteCap( outInfo, "doc_mentions_capped", nullptr, docsCapped, 0 ); + // whose docs were never looked at (see collectRefusedDocLifts). + collectRefusedDocLifts( g, lensRank, order, stoppedAt, topN, refusedDocs ); + std::sort( refusedDocs.begin(), refusedDocs.end() ); // one doc under two anchors is ONE refusal + refusedDocs.erase( std::unique( refusedDocs.begin(), refusedDocs.end() ), refusedDocs.end() ); + // doc_mentions_total= is lifted + refused: how many docs the caps had to choose from. It was nullptr — + // a bare boolean saying content was withheld and neither how much nor how to get it, which is §9-3 of + // docs/METHODOLOGY.md unmet by the same file whose mention_tokens_capped/mention_syms_capped both pass + // a total. `doc_mentions=` on the root already carries the shown half, so total - doc_mentions is the gap. + noteCap( outInfo, "doc_mentions_capped", "doc_mentions_total", !refusedDocs.empty(), + std::uint64_t( liftedDocs ) + refusedDocs.size() ); if( liftedDocs == 0 ) { diff --git a/src/situ.h b/src/situ.h index 6bfe60c36..a2f14148e 100644 --- a/src/situ.h +++ b/src/situ.h @@ -1092,12 +1092,19 @@ inline void writeTestGateReport( std::FILE* out, const IngestResult& ing, const // §P11.4: this gate EXITS 4 on the obligation, so its rows carry the command that discharges it — where // one is derivable. Absent run= = not derivable (testmap.h states why a fallback would be a lie). const TestRunnerIndex gateRunners( ing ); + // shown_tests= / tests_capped= are DERIVED from the rows this document actually emits, not asserted. + // tests_capped= was the string literal "0" — a disclosure that could never become "1", so if a row + // cap were ever added the attribute would keep saying nothing was cut while something was. It is kept + // present at 0 rather than omitted, because pageview.h rule 1 pairs shown_*/=*_capped per LISTING and its + // sibling untested_capped="0" is pinned by test/testgatepagecheck.sh (a') and test/impactpartitioncheck.sh: + // dropping one half of a documented pair is a new inconsistency, not a fix for this one. + const std::size_t shownTests = r.testRows.size() + r.shellGates.obligations.size(); rw::emitTo( out, "", r.changedFiles, r.impactedSymbols, testRows, r.untested.size(), - testRows, shownRows, shownRows < r.untested.size() ? 1 : 0, + shownTests, shownTests < testRows ? 1 : 0, shownRows, shownRows < r.untested.size() ? 1 : 0, scriptGatesUnmodelledCount( ing ), r.shellGates.registered, r.shellGates.mapped, r.shellGates.unresolvedDynamic, kTestGateCcxBarMirror, // P8 (L7): ccx_bar= graphCountFloorAttrXml( g ).c_str(), // M15: gauge + counts_floor="1", the one splice every graph-floored root shares @@ -1162,12 +1169,15 @@ inline void writeTestGateReportJson( std::FILE* out, const IngestResult& ing, co const TestRunnerIndex gateRunnersJ( ing ); // P3 (L7): the root's next= needs the runner index before the rows const bool tgJHasRows = ( testRows > 0 || !r.untested.empty() ); const std::string tgJRootJson = ( root.empty() || !tgJHasRows ) ? std::string() : ( ",\"root\":\"" + jsonStr( root ) + "\"" ); + // The XML twin's derived pair, mirrored key-for-key: "tests_capped":false was a literal here too. + const std::size_t shownTestsJ = r.testRows.size() + r.shellGates.obligations.size(); rw::emitTo( out, "{{\"changed\":{},\"impacted\":{},\"tests\":{},\"untested\":{}" - ",\"shown_tests\":{},\"tests_capped\":false,\"shown_untested\":{},\"untested_capped\":{}" + ",\"shown_tests\":{},\"tests_capped\":{},\"shown_untested\":{},\"untested_capped\":{}" ",\"script_gates_unmodelled\":{},\"script_gates_registered\":{},\"script_gates_mapped\":{}" ",\"script_gates_unresolved_dynamic\":{},\"ccx_bar\":{}{}{},\"at\":{}{}{},\"tests_to_run\":[", r.changedFiles, r.impactedSymbols, testRows, r.untested.size(), - testRows, shownRows, shownRows < r.untested.size() ? "true" : "false", + shownTestsJ, shownTestsJ < testRows ? "true" : "false", shownRows, + shownRows < r.untested.size() ? "true" : "false", scriptGatesUnmodelledCount( ing ), r.shellGates.registered, r.shellGates.mapped, r.shellGates.unresolvedDynamic, kTestGateCcxBarMirror, graphCountFloorAttrJson( g ).c_str(), // M15: the JSON twin's gauge + "counts_floor":true rw::cstr( pageJson ), atJson.c_str(), tgJRootJson.c_str(), // M12: root= rides only when the document has rows (same gate as the XML twin) diff --git a/test/mentioncapcheck.sh b/test/mentioncapcheck.sh index 0fee78934..88365725b 100755 --- a/test/mentioncapcheck.sh +++ b/test/mentioncapcheck.sh @@ -37,6 +37,12 @@ # MUTATION CONTROL: assertion 2 of every arm is exactly what a revert of this fix removes, and assertion 1 # proves the fixture still reaches the reverted code. Run against a binary built from the parent commit — # RIPWIRE_BIN=/ripwire bash test/mentioncapcheck.sh +# +# (H) A BARE BOOLEAN IS NOT A DISCLOSURE. mention_files_capped= and doc_mentions_capped= passed nullptr +# as their total, so the caller learned that content was withheld and neither how much nor how to +# get it — §9-3 unmet by the same file whose mention_tokens_capped/mention_syms_capped both carry +# one. H1/H3 require the total beside each flag and require it to EXCEED the shown count; H2 +# requires it absent when the cap did not fire, so an uncut answer stays byte-identical. # — and every assertion 2 must FAIL while every assertion 1 still passes. That is the red run this gate was # written from, before the code existed. # @@ -63,6 +69,8 @@ anchorFiles(){ printf '%s' "$1" | grep -o 'mention anchor: [0-9]* file' | grep - anchorSyms(){ printf '%s' "$1" | grep -o '+ [0-9]* symbols named' | grep -o '[0-9]*' | head -1; } attr(){ printf '%s' "$2" | grep -o "$1=\"[0-9]*\"" | head -1; } has(){ printf '%s' "$2" | grep -q "$1"; } +# attr() returns `name="N"` (the form the older arms compare as text); num() is its VALUE, for arithmetic. +num(){ attr "$1" "$2" | grep -o '[0-9]*' | head -1; } # =================================================================================================== # (A) kMentionMaxRawTokens=16 — the extraction window over the TASK TEXT @@ -465,5 +473,41 @@ else ok "G5 (skipped: no xmllint)" fi +# ── (H) A BARE BOOLEAN IS NOT A DISCLOSURE ───────────────────────────────────────────────────────── +# mention_files_capped= and doc_mentions_capped= were noteCap(..., nullptr, ...): the caller was told that +# something had been withheld and neither how much nor how to get it. docs/METHODOLOGY.md §9-3 says a cut +# is terminal only when the caller can finish in one more KNOWN call, and the sibling caps in the same +# file — mention_tokens_capped, mention_syms_capped — have always passed a total. Both now do. +if has 'mention_files_capped="1"' "$bWide"; then + hTot="$( num mention_files_total "$bWide" )" + hSeen="$( anchorFiles "$bWide" )" + if [ -n "$hTot" ] && [ -n "$hSeen" ] && [ "$hTot" -gt "$hSeen" ]; then + ok "H1 mention_files_capped=\"1\" carries mention_files_total=\"$hTot\" against $hSeen lifted — the gap is nameable" + else + no "H1 the file cut disclosed no usable total (total='$hTot' lifted='$hSeen') — a bare boolean is not a disclosure" + fi +else + no "H1 fixture broken: the wide file fixture no longer discloses a cut" +fi +# the total must be ABSENT when the cap did not fire — a zero-cost run stays zero-cost +if has 'mention_files_total' "$bNarrow"; then + no "H2 an uncut run paid for mention_files_total= — the attribute is not gated on the cut" +else + ok "H2 an uncut run carries no mention_files_total= (the disclosure costs nothing when nothing was cut)" +fi +# the doc half, on the tool's own tree: doc_mentions= is the shown count, doc_mentions_total= the choice set +hDoc="$( run "$ROOT" --for="pagerank power iteration" )" +if has 'doc_mentions_capped="1"' "$hDoc"; then + dTot="$( num doc_mentions_total "$hDoc" )" + dSeen="$( num doc_mentions "$hDoc" )" + if [ -n "$dTot" ] && [ -n "$dSeen" ] && [ "$dTot" -gt "$dSeen" ]; then + ok "H3 doc_mentions_capped=\"1\" carries doc_mentions_total=\"$dTot\" beside doc_mentions=\"$dSeen\"" + else + no "H3 the doc cut disclosed no usable total (total='$dTot' shown='$dSeen')" + fi +else + ok "H3 (no doc cut on this tree today — nothing to check, and nothing claimed)" +fi + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "FAILURES" exit "$fail" diff --git a/test/testgatepagecheck.sh b/test/testgatepagecheck.sh index 1cec8e8a7..d9858b80c 100755 --- a/test/testgatepagecheck.sh +++ b/test/testgatepagecheck.sh @@ -190,5 +190,40 @@ ok "PC-2: every runtime-honored verb is named in --help's HONORED-by list" printf '%s\n' "$runtoks" | grep -qxF -- '--test-gate' && ok "PC-2: --test-gate is in the runtime honored set" || no "PC-2: --test-gate missing from the runtime honored set" printf '%s\n' "$helptoks" | grep -qxF -- '--test-gate' && ok "PC-2: --test-gate is in --help's HONORED-by list" || no "PC-2: --test-gate missing from --help's HONORED-by list" +# ── (d) tests_capped= is DERIVED, not asserted ───────────────────────────────────────────────────────────── +# It was the string literal "0" in both dialects (src/situ.h) — a disclosure that could never become "1", +# so a row cap added later would keep saying nothing was cut while something was. The invariant that +# replaces the literal: shown_tests= is the number of rows this document ACTUALLY emitted, and +# tests_capped= is shown_tests < tests. Checked against the emitted rows, not against another attribute. +# Its own fixture, so the counted quantities are non-zero: $R has no test file at all, and an arm whose +# every number is 0 is an arm that cannot tell a derivation from a literal. +TG="$TMP/tgrepo"; mkdir -p "$TG/src" "$TG/test" +printf 'int lib0() { return 0; }\nint lib1() { return 1; }\nint lib2() { return 2; }\n' > "$TG/src/lib.cpp" +printf '#include "../src/lib.cpp"\nint test_lib0() { return lib0(); }\n' > "$TG/test/lib0_test.cpp" +printf '#include "../src/lib.cpp"\nint test_lib1() { return lib1(); }\n' > "$TG/test/lib1_test.cpp" +D="$( run "$TG" --test-gate=src/lib.cpp )" +DTROWS="$( printf '%s' "$D" | grep -o ' rows emitted" \ + || no "(d) wrong (shown_tests=$DSHOWN rows=$DTROWS tests=$DTOTAL tests_capped=$DCAP want=$DWANT)" +DJ="$( run "$TG" --test-gate=src/lib.cpp --json )" +DJCAP="$( jattr "$DJ" tests_capped )"; DJSHOWN="$( jattr "$DJ" shown_tests )" +DJWANT="$DWANT" # jattr() already normalises JSON true/false to 1/0 +{ [ "$DJSHOWN" = "$DSHOWN" ] && [ "$DJCAP" = "$DJWANT" ]; } \ + && ok "(d-json) the JSON dialect mirrors it key-for-key (shown_tests=$DJSHOWN tests_capped=$DJCAP)" \ + || no "(d-json) mirror broken (shown_tests=$DJSHOWN vs $DSHOWN, tests_capped=$DJCAP vs $DJWANT)" +# THE CONTROL, on a synthetic COPY of the document. A literal "0" satisfies the arm above on every tree +# where nothing is cut — which is every tree today — so without this the arm is green forever and proves +# nothing about the derivation. Rewrite the copy so shown_tests is BELOW tests with tests_capped still "0", +# exactly the shape the literal would produce under a future cap, and require the check to reject it. +DFAKE="$( printf '%s' "$D" | sed 's/ tests="[0-9]*"/ tests="99"/' )" +FSHOWN="$( attr "$DFAKE" shown_tests )"; FTOTAL="$( attr "$DFAKE" tests )"; FCAP="$( attr "$DFAKE" tests_capped )" +FWANT=0; [ "${FSHOWN:-0}" -lt "${FTOTAL:-0}" ] && FWANT=1 +{ [ "$FSHOWN" != "$FTOTAL" ] && [ "$FCAP" != "$FWANT" ]; } \ + && ok "(d) control: a document claiming tests_capped=\"$FCAP\" with shown_tests=$FSHOWN of $FTOTAL is REJECTED — the arm can go red" \ + || no "(d) control: the arm accepted a document whose tests_capped contradicts its own counts — (d) is inert" + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 04dceeaa68cdee6581fd04f8a9a1f9b7ed700491 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:53:27 -0400 Subject: [PATCH 19/73] feat(help-task): route the three verbs with none, and give the router the whole skill catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 2026-09-10 audit measured --help-task at 3 recommends over 39 phrasings of the 13 surfaces added since 2026-08-28 (F-R1-08), and found the router able to name 8 of the 16 shipped skills (F-R1-09) — nine skill directories no --help-task answer could ever point at. Three of the unrouted surfaces are VERBS, not shaping flags: --handoff (which has its own shipped skill), --plan-lint, and the PROSE form of --from-trace (looksLikeTrace matches a PASTED artifact; "I have a sanitizer report" carries none of its literals, so the #108 name-ladder work was unreachable from prose). Ten intents in a catalogTaskChoice tier placed LAST in directTaskChoice, so every older and more specific route keeps its rows: handoff-brief --handoff, plan-lint --plan-lint=FILE, trace-prose --from-trace=-, scan-skills/scan-skill, opt-remark --for=TASK, architecture-health --deps, quality-check --quality-delta, perf-symbol --around=SYM, graph-query --graph-query=EXPR, maintenance-risk --hotspots. Conjunctive evidence in the instrumentedTaskChoice shape throughout; the value-carrying ones fire only when the task supplies the value. Two worth naming: opt-remark is the ONLY skill with no verb of its own (a contributor workflow around clang remarks and a profiling build), so it routes to the ranked lens and its reason says exactly that rather than implying a dedicated surface; graph-query COMPOSES an expression out of what the task supplied — the symbol it named, the direction it asked for — with a stated default depth, and the gate unquotes what the router emitted and runs it through the real verb. measurement before after skills the router can name (of 16) 8 16 the audit's 39 surface phrasings, recommends 3 9 corpus split=test (n=114) accuracy / coverage 0.754/0.627 0.939/0.907 corpus split=dev (n=111) accuracy / coverage — 0.946/0.929 corpus split=all (n=225) accuracy / coverage 0.809/0.730 0.942/0.918 precision / harmful / neg-specificity, all splits 1.0/0/1.0 1.0/0/1.0 the 189 rows that predate this tier — 0 differing RED-FIRST IS THE GATE, NOT THE EVAL: coverage has no floor by the round-1 rule, so the eval exits 0 either way. Eleven taskroutecheck arms fail against the pre-change binary (each abstained with score="0"), plus the two execution arms; and the skill-vocabulary arm — which reads BOTH sides from disk, the skill directories and the names src/taskroute.h can emit — fails against the pre-change source, naming all eight unreachable skills. That arm is the durable half: a new skill shipping without a route now fails as loudly as a route naming a skill with no directory. The 30 phrasings still declined are declined BY DESIGN and gated as such: six shaping flags (--scope, --slice-depth, --slice-flow, --allow-dirty, --no-ignore, --no-post-check) are modifiers on other verbs, --pin-census is eval-only, and the value-carrying abstentions keep the 2026-08-28 rule that the router may not emit a command the verb would refuse. Corpus +36 rows (30 positives, 3 per intent; 6 negatives that must NOT route), every routing outcome verified against a live binary before insertion. Two corrections that verification caught, recorded in PROVENANCE rather than smoothed over: a file that names ITSELF a plan (PLAN_*.md/DESIGN_*.md) is now surface evidence the prose need not repeat, and "before i commit" was re-weighted below the quality-check floor — a TIMING word, not a quality word, which at its first weight stole "lint the plan file layout before I commit it" from the plan-lint abstention. Map untouched: default map and --for byte-identical to the pre-change binary, xmllint clean. Seal 1719aea95449e222718ec38151d2bd6998a95e1dd070038baa0b6e28fd0c9cf5 (189 -> 225 rows); screen unchanged at 2 flagged lines despite the large amount of new card vocabulary. quality-delta gating=0. Co-Authored-By: Claude Fable 5.1 --- docs/EVALS.md | 56 ++++++++++ src/taskroute.h | 182 ++++++++++++++++++++++++++++++-- test/taskroutecheck.sh | 72 +++++++++++++ test/taskroutefix/PROVENANCE.md | 63 +++++++++++ test/taskroutefix/prompts.tsv | 36 +++++++ 5 files changed, 403 insertions(+), 6 deletions(-) diff --git a/docs/EVALS.md b/docs/EVALS.md index 2ac0e0977..9649faf4d 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -2205,6 +2205,62 @@ checks into their own `flowTaskChoice` function (mirroring the existing `instrum extraction) and by inlining the small filler-word loop directly rather than introducing a shared helper that collided token-for-token with `weakSymbolCandidate`'s existing shape. +### `--help-task` catalog tier: the verbs and the skills with no route (2026-09-10) + +**Two measurements, one cause.** `--help-task` recommended on **3 of 39** phrasings of the 13 surfaces +added since the 2026-08-28 audit (F-R1-08), and could name **8 of the 16** shipped skills (F-R1-09) — +nine skill directories existed that no `--help-task` answer could ever point at. `--help-task` and the +skill catalog were two routers with two vocabularies. Three of the unrouted surfaces are VERBS rather +than shaping flags: `--handoff` (which has its own shipped skill), `--plan-lint`, and the PROSE form of +`--from-trace` — `looksLikeTrace` matches a PASTED artifact (`AddressSanitizer:`, `#0 … in`), and "I +have a sanitizer report" contains none of those literals, so the #108 name-ladder work was unreachable +from prose. + +**Ten intents, in a `catalogTaskChoice` tier placed LAST in `directTaskChoice`** so every older, more +specific route keeps its rows: `handoff-brief` → `--handoff`, `plan-lint` → `--plan-lint=FILE`, +`trace-prose` → `--from-trace=-`, `scan-skills`/`scan-skill` → `--scan-skills` / `--scan-skill=FILE`, +`opt-remark` → `--for=TASK`, `architecture-health` → `--deps`, `quality-check` → `--quality-delta`, +`perf-symbol` → `--around=SYM`, `graph-query` → `--graph-query=EXPR`, `maintenance-risk` → +`--hotspots`. Each takes conjunctive evidence in the shape `instrumentedTaskChoice` established, and the +value-carrying ones fire only when the task supplies the value. + +Two of them are worth stating plainly rather than listing. **`opt-remark` is the one skill with no verb +of its own** — it is a contributor workflow around clang remarks and a profiling build — so it routes to +the ranked lens and its reason string says exactly that, instead of implying a dedicated surface exists. +**`graph-query` composes an expression** out of what the task supplied (the symbol it named, the +direction it asked for) with a stated default depth, the same way `--grep-context=2` and +`--slice-flow=back` are defaults; the gate unquotes what the router emitted and runs it through the real +verb, so a composed expression the verb would refuse fails the gate rather than the user. + +| measurement | before | after | +| --- | ---: | ---: | +| skills the router can name (of 16, `ripwire-router` excluded) | **8** | **16** | +| audit's 39 surface phrasings, recommends | **3** | **9** | +| corpus `split=test` (n=114) accuracy / coverage | 0.754 / 0.627 | **0.939 / 0.907** | +| corpus `split=dev` (n=111) accuracy / coverage | — | 0.946 / 0.929 | +| corpus `split=all` (n=225) accuracy / coverage | 0.809 / 0.730 | **0.942 / 0.918** | +| precision / harmful / neg-specificity, every split | 1.000 / 0.000 / 1.000 | 1.000 / 0.000 / 1.000 | +| the 189 rows that predate this tier, (status, intent, resolved_symbols) | — | **0 differing** | + +The remaining 30 of 39 are declined by design and are gated as such: six SHAPING flags (`--scope`, +`--slice-depth`, `--slice-flow`, `--allow-dirty`, `--no-ignore`, `--no-post-check`) are modifiers on +other verbs and not commands a one-command router can recommend alone; `--pin-census` is eval-only; and +the value-carrying abstentions (`--edit-plan=FILE`, `--grep=LIT --handles`, `--plan-lint` with no file +named) keep the 2026-08-28 rule that the router may not emit a command the verb would refuse. + +**Red-first is the GATE here, not the eval.** Coverage has no floor by the round-1 rule, so the eval +exits 0 either way; eleven `taskroutecheck` arms fail against the pre-change binary (every one abstained +with `score="0"`), plus the two execution arms, and the skill-vocabulary arm — which reads BOTH sides +from disk, the skill directories and the names `src/taskroute.h` can emit — fails against the pre-change +source, naming all eight unreachable skills. That arm is the durable half: a new skill that ships +without a route now fails as loudly as a route naming a skill that does not exist. + +**Two corrections the pre-insertion verification caught**, recorded because "every row verified before +insertion" is only worth something if the failures are shown too. A plan file that names ITSELF +(`PLAN_*.md`, `DESIGN_*.md`) is now surface evidence the prose need not repeat. And `"before i commit"` +was re-weighted below the quality-check floor: it is a TIMING word, not a quality word, and at its first +weight it stole *"lint the plan file layout before I commit it"* from the plan-lint abstention. + ### The four restored stop rules become measurable (2026-09-10) **The defect (audit F-R1-03).** #112 restored four frontmatter STOP RULES to the skill descriptions — diff --git a/src/taskroute.h b/src/taskroute.h index 575e6a83d..92ac33216 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -551,9 +551,11 @@ inline void addLexical( std::vector& choices, const char* id, const } } -// A plan path the user actually WROTE. The router never invents one: --edit-plan refuses a file that is -// not there, and recommending a command the verb refuses is a prerequisite violation, not a suggestion. -inline std::string firstJsonPathToken( std::string_view task ) +// A path the user actually WROTE, recognised by its extension. The router never invents one: --edit-plan +// and --plan-lint both refuse a file that is not there, and recommending a command the verb refuses is a +// prerequisite violation, not a suggestion. One extractor for every value-carrying path route, so the two +// cannot disagree about what counts as a written path. +inline std::string firstPathTokenWithSuffix( std::string_view task, std::initializer_list suffixes ) { constexpr std::string_view kBreaks = " \t\n\r\"'`(),;"; for( std::size_t i = 0; i < task.size(); ) @@ -568,16 +570,28 @@ inline std::string firstJsonPathToken( std::string_view task ) { end = task.size(); } - const std::string_view token = task.substr( begin, end - begin ); - if( token.ends_with( ".json" ) || token.ends_with( ".ndjson" ) ) + std::string_view token = task.substr( begin, end - begin ); + while( !token.empty() && ( token.back() == '.' || token.back() == '?' || token.back() == '!' ) ) + { + token.remove_suffix( 1 ); + } + for( const std::string_view suffix : suffixes ) { - return std::string( token ); + if( token.size() > suffix.size() && token.ends_with( suffix ) ) + { + return std::string( token ); + } } i = end + 1; } return {}; } +inline std::string firstJsonPathToken( std::string_view task ) +{ + return firstPathTokenWithSuffix( task, { ".json", ".ndjson" } ); +} + // Routes for surfaces whose trigger is a NAME rather than a phrase-scoring shape: the flag is asked for by // something close to its own vocabulary, so a weighted score would only add noise. Each requires // conjunctive evidence — the surface word AND an intent word — so a passing mention never routes. The two @@ -689,6 +703,155 @@ inline std::optional flowTaskChoice( std::string_view task, std::st return std::nullopt; } +// ── the catalog tier: verbs and skills the router could not name at all ─────────────────────────────── +// The 2026-09-10 audit measured `--help-task` at 3 recommends over 39 phrasings of the 13 surfaces added +// since 2026-08-28 (F-R1-08), and found the router able to name 8 of the 16 shipped skills (F-R1-09) — +// `--help-task` and the skill catalog were two routers with two vocabularies. These routes close both +// gaps for the surfaces that are VERBS rather than shaping flags. They sit LAST in directTaskChoice on +// purpose: every route above them is older, more specific, and keeps its rows unchanged. +// +// Each requires conjunctive evidence in the same shape instrumentedTaskChoice uses — the surface asked +// for close to its own vocabulary, plus a second word that says it is being asked FOR — and the two that +// carry a user-supplied value fire only when the task supplies it. What is deliberately NOT here, and +// why: `--scope`, `--slice-depth`, `--slice-flow`, `--allow-dirty`, `--no-ignore`, `--no-post-check` are +// SHAPING flags on other verbs, not commands a one-command router can recommend on their own. +inline std::optional catalogTaskChoice( std::string_view task, std::string_view lower, + const std::string& root, const std::vector& symbols ) +{ + const std::string ripRoot = "ripwire " + shSingleQuote( root ) + " "; + // handoff: brief SOMEONE ELSE. The discriminator against orient is the second party — a successor, a + // teammate, the next session — never the speaker's own understanding. + const int handoffScore = phraseScore( lower, { { "hand off", 9 }, { "hand this off", 9 }, { "handing off", 9 }, + { "hand-off", 8 }, { "handover", 8 }, { "hand over", 8 }, + { "takes it over", 8 }, { "taking over", 7 }, { "take it over", 7 }, + { "next session", 7 }, { "successor", 7 }, { "picks this up", 7 }, + { "whoever", 6 }, { "going on leave", 7 }, { "brief the next", 8 }, + { "brief someone", 8 }, { "for the next agent", 8 }, + { "onboard", 5 }, { "teammate", 5 } } ); + if( handoffScore >= 7 ) + { + return RouteChoice{ "handoff-brief", "ripwire-handoff", "briefing a SECOND party (successor/teammate/next session)", + ripRoot + "--handoff", 100, 79 }; + } + // plan-lint: a PLAN/DESIGN file's structure. Value-carrying — the verb refuses a file that is not + // there, so the route fires only when the task names one. + const std::string planDoc = firstPathTokenWithSuffix( task, { ".md", ".markdown" } ); + const std::string planDocL = lowerAscii( planDoc ); + // The file may name ITSELF as the plan (PLAN_CACHE.md, DESIGN_NOTES.md) — that is surface evidence + // the task's prose does not have to repeat. + if( ( has( lower, "plan" ) || has( lower, "design doc" ) || has( lower, "design document" ) + || has( planDocL, "plan" ) || has( planDocL, "design" ) ) + && ( has( lower, "lint" ) || has( lower, "structure" ) || has( lower, "well-formed" ) || has( lower, "sections" ) + || has( lower, "shape" ) || has( lower, "check" ) ) ) + { + if( !planDoc.empty() ) + { + return RouteChoice{ "plan-lint", "ripwire-before-you-build", "plan/design structure wording plus a named markdown file", + commandWithValue( root, "--plan-lint=", planDoc ), 100, 78 }; + } + } + // trace-prose: the user SAYS they are holding a trace instead of pasting one. looksLikeTrace matches a + // pasted artifact (`AddressSanitizer:`, `#0 … in`), and a sanitizer REPORT described in words contains + // none of those literals, so the #108 name-ladder work was unreachable from prose (F-R1-08 §1.2). + const bool holdsTrace = has( lower, "sanitizer report" ) || has( lower, "asan report" ) || has( lower, "crash log" ) + || has( lower, "compiler error" ) || has( lower, "backtrace" ) || has( lower, "core dump" ) + || has( lower, "build error" ) || has( lower, "panic message" ); + if( holdsTrace + && ( has( lower, "map" ) || has( lower, "onto" ) || has( lower, "which symbol" ) || has( lower, "indexed" ) + || has( lower, "translate" ) || has( lower, "i have" ) || has( lower, "here is" ) || has( lower, "frames" ) ) ) + { + return RouteChoice{ "trace-prose", "ripwire-find-bug", "a trace/report described rather than pasted; pass it on stdin", + ripRoot + "--from-trace=-", 100, 77 }; + } + // security-scan: vetting something UNTRUSTED before installing it. --scan-skills defaults its directory, + // so the valueless form is a real command; a named file upgrades it to --scan-skill=FILE. + const int scanScore = phraseScore( lower, { { "before installing", 9 }, { "before i install", 9 }, + { "prompt injection", 9 }, { "exfiltration", 9 }, { "untrusted", 7 }, + { "vet", 6 }, { "audit", 4 }, { "safe to install", 9 }, + { "skill file", 6 }, { "mcp.json", 6 }, { "skill", 3 } } ); + if( scanScore >= 9 && ( has( lower, "skill" ) || has( lower, "mcp" ) || has( lower, "install" ) ) ) + { + const std::string skillFile = firstPathTokenWithSuffix( task, { ".md", ".markdown", ".json", ".sh" } ); + return skillFile.empty() + ? RouteChoice{ "scan-skills", "ripwire-security-scan", "pre-install vetting wording; --scan-skills defaults its directory", + ripRoot + "--scan-skills", 100, 76 } + : RouteChoice{ "scan-skill", "ripwire-security-scan", "pre-install vetting wording plus a named file", + commandWithValue( root, "--scan-skill=", skillFile ), 100, 76 }; + } + // opt-remarks: clang optimization remarks while building ripwire itself. The ONLY skill with no verb of + // its own — the ranked lens is what finds the symbol a remark names, and the reason says exactly that + // rather than implying a dedicated surface exists. + if( has( lower, "not vectorized" ) || has( lower, "will not be inlined" ) || has( lower, "-rpass" ) + || has( lower, "opt-record" ) || has( lower, "optimization remark" ) || has( lower, "optimisation remark" ) ) + { + return RouteChoice{ "opt-remark", "ripwire-opt-remarks", "a clang optimization remark; the ranked lens locates the symbol it names", + commandWithValue( root, "--for=", task ), 100, 75 }; + } + // architecture-health: cycles, god files, propagation. --arch=FILE is the GATING form and needs a rules + // file the task names; without one, --deps is the real, runnable overview the same skill leads with. + const int layersScore = phraseScore( lower, { { "layering", 9 }, { "layer violation", 9 }, { "dependency mess", 9 }, + { "module boundaries", 9 }, { "circular dependenc", 9 }, + { "dependency cycle", 9 }, { "god file", 8 }, { "godfile", 8 }, + { "propagation cost", 9 }, { "architecture health", 9 }, + { "reaches into the database", 9 }, { "cycles", 5 } } ); + if( layersScore >= 8 ) + { + return RouteChoice{ "architecture-health", "ripwire-layers", "architecture-health wording (--arch=FILE is the gating form and needs a rules file)", + ripRoot + "--deps", 100, 74 }; + } + // quality-bar: what YOU just wrote, before you call it done. Deliberately narrow so it cannot steal the + // dirty-worktree review route below, whose wording is about a DIFF and a push rather than about debt. + // "before i commit" is a TIMING word, not a quality word — on its own it also fits linting a plan or + // running a gate, and at the old weight it stole "lint the plan file layout before I commit it" from + // the plan-lint abstention above. Weighted below the floor so it can only ever CONFIRM a quality word. + const int qualityScore = phraseScore( lower, { { "call it done", 9 }, { "before i commit", 6 }, + { "new debt", 9 }, { "made anything worse", 9 }, + { "made it worse", 8 }, { "got worse", 8 }, + { "code i just wrote", 9 }, { "quality of what i", 9 }, + { "verify the cleanup", 9 }, { "quality bar", 8 } } ); + if( qualityScore >= 9 ) + { + return RouteChoice{ "quality-check", "ripwire-quality-bar", "own-code quality wording (what got WORSE), not merge safety", + ripRoot + "--quality-delta", 100, 73 }; + } + // perf-target: a MEASURED profile that NAMES a symbol. Static metrics are not runtime heat, so this + // needs the profile wording AND the one symbol the profile named — never the wording alone. + const int perfScore = phraseScore( lower, { { "profiler", 9 }, { "flame graph", 9 }, { "flamegraph", 9 }, + { "perf sample", 9 }, { "perf record", 9 }, { "hot symbol", 8 }, + { "hot path", 7 }, { "hottest", 8 }, { "benchmark says", 8 }, + { "cpu time", 7 }, { "profile names", 8 } } ); + if( symbols.size() == 1 && perfScore >= 7 ) + { + return RouteChoice{ "perf-symbol", "ripwire-perf-target", "a measured profile plus the one symbol it names", + commandWithValue( root, "--around=", symbols[0] ), 100, 72 }; + } + // graph-query: a closure question the fixed verbs cannot phrase. The EXPRESSION is built only out of + // what the task supplied — the symbol it named and the direction it asked for — and the depth is a + // stated default the reason names, the same way --grep-context=2 and --slice-flow=back are. + const int closureScore = phraseScore( lower, { { "can reach", 8 }, { "that reach", 8 }, { "everything that reaches", 9 }, + { "transitively", 7 }, { "within one hop", 8 }, { "within two hops", 8 }, + { "graph query", 9 }, { "call graph question", 9 }, { "fan-in", 7 } } ); + if( symbols.size() == 1 && closureScore >= 7 ) + { + const bool outward = has( lower, "reachable from" ) || has( lower, "everything it calls" ) || has( lower, "downstream of" ); + const std::string expr = std::string( outward ? "callees(name(\"" : "callers(name(\"" ) + symbols[0] + "\"),3)"; + return RouteChoice{ "graph-query", "ripwire-graph-query", "a bounded-closure question plus one named symbol (depth 3 is the default; raise it)", + commandWithValue( root, "--graph-query=", expr ), 100, 71 }; + } + // fresh-eyes: maintenance risk in code the speaker did NOT write. LAST of the catalog tier because its + // vocabulary is the broadest, so every more specific reading above gets first refusal. + const int riskScore = phraseScore( lower, { { "gnarly", 9 }, { "where is the rot", 9 }, { "the rot", 8 }, + { "safe to touch", 9 }, { "god object", 9 }, { "maintenance risk", 9 }, + { "did not write", 8 }, { "didn't write", 8 }, { "i inherited", 8 }, + { "inherited", 6 }, { "where maintenance hurts", 9 }, { "bus factor", 9 } } ); + if( riskScore >= 8 ) + { + return RouteChoice{ "maintenance-risk", "ripwire-fresh-eyes", "maintenance-risk wording about code the speaker did not write", + ripRoot + "--hotspots", 100, 70 }; + } + return std::nullopt; +} + inline std::optional directTaskChoice( std::string_view task, std::string_view lower, const std::string& root, const std::vector& symbols ) { @@ -740,6 +903,13 @@ inline std::optional directTaskChoice( std::string_view task, std:: { return flow; } + // catalog tier LAST: the verbs and skills the router could not name at all before 2026-09-10. Every + // route above this line is older and more specific and keeps its rows unchanged (measured: all 189 + // corpus rows byte-identical on status/intent across this addition). + if( std::optional catalog = catalogTaskChoice( task, lower, root, symbols ) ) + { + return catalog; + } return std::nullopt; } diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 39fe321db..63672eb25 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -223,6 +223,78 @@ ALRUN="$( "$BIN" "$REPO" --no-cache --slice='@router.cpp:10' )"; rc=$? && ok "the emitted at-line @FILE:LINE command runs and seeds at the named line" \ || no "the emitted at-line @FILE:LINE command failed to run (rc=$rc)" +# ── the catalog tier: verbs and skills the router could not name at all (2026-09-10) ────────────────── +# F-R1-08: --help-task recommended on 3 of 39 phrasings of the 13 surfaces added since 2026-08-28, and +# three of the unrouted ones were VERBS — --handoff (which has its own shipped skill), --plan-lint, and +# the PROSE form of --from-trace (looksLikeTrace matches a PASTED artifact; "I have a sanitizer report" +# contains none of its literals). F-R1-09: the router could name 8 of the 16 shipped skills. +# Every recommend arm below is red against a pre-change binary: all of them abstained with score="0". +HO="$( route 'I am going on leave next week - put together a brief on the scheduler for whoever takes it over' )" +case "$HO" in *'status="recommend"'*'intent="handoff-brief"'*'skill="ripwire-handoff"'*'--handoff'*) ok "briefing a second party -> --handoff";; *) no "handoff route wrong: $HO";; esac +HO0="$( route 'we handed the account off to support last week, any update on the customer?' )" +case "$HO0" in *'--handoff'*) no "an account handover minted a --handoff route: $HO0";; *) ok "prose about handing over anything else mints no --handoff";; esac +PL="$( route 'check that docs/PLAN_NEXT.md is well-formed as a plan document' )" +case "$PL" in *'status="recommend"'*'intent="plan-lint"'*'--plan-lint='*'docs/PLAN_NEXT.md'*) ok "plan-structure wording + a named markdown file -> --plan-lint=FILE";; *) no "plan-lint route wrong: $PL";; esac +# Value-carrying, like --edit-plan: the verb refuses a file that is not there, so no file, no command. +PL0="$( route 'can you lint the structure of our planning docs in general?' )" +case "$PL0" in *'--plan-lint='*) no "plan-lint invented a file the task never named: $PL0";; *) ok "plan-lint abstains rather than invent a plan document";; esac +TP="$( route 'I have a sanitizer report from last night - map it onto the indexed symbols' )" +case "$TP" in *'status="recommend"'*'intent="trace-prose"'*'--from-trace=-'*) ok "a trace DESCRIBED rather than pasted -> --from-trace=-";; *) no "trace-prose route wrong: $TP";; esac +SS="$( route 'someone sent me a skills bundle - is it safe to install, any prompt injection in there?' )" +case "$SS" in *'status="recommend"'*'intent="scan-skills"'*'skill="ripwire-security-scan"'*'--scan-skills'*) ok "pre-install vetting -> --scan-skills";; *) no "scan-skills route wrong: $SS";; esac +SS1="$( route 'check tools/helper.md for exfiltration before installing it as a skill' )" +case "$SS1" in *'intent="scan-skill"'*'--scan-skill='*'tools/helper.md'*) ok "a named file upgrades the scan to --scan-skill=FILE";; *) no "scan-skill route wrong: $SS1";; esac +AH="$( route 'do we have a dependency mess in here - any circular dependencies or god file?' )" +case "$AH" in *'status="recommend"'*'intent="architecture-health"'*'skill="ripwire-layers"'*'--deps'*) ok "architecture-health wording -> --deps";; *) no "architecture-health route wrong: $AH";; esac +QC="$( route 'before I call it done - did my change make anything worse?' )" +case "$QC" in *'status="recommend"'*'intent="quality-check"'*'skill="ripwire-quality-bar"'*'--quality-delta'*) ok "own-code quality wording -> --quality-delta";; *) no "quality-check route wrong: $QC";; esac +# The narrow quality vocabulary must not steal the dirty-worktree review route, whose words are about a +# DIFF and a push. (This repo is CLEAN here, so review-diff cannot fire either way — assert the intent.) +QC0="$( route 'Reviewing my own diff now - am I ready to push and is this safe to merge?' )" +case "$QC0" in *'intent="quality-check"'*) no "diff-review wording was stolen by quality-check: $QC0";; *) ok "diff-review wording is not a quality-delta request";; esac +PS="$( route 'the profiler puts targetSymbol at the top - what is around it' )" +case "$PS" in *'status="recommend"'*'intent="perf-symbol"'*'skill="ripwire-perf-target"'*'--around='*'targetSymbol'*) ok "a measured profile + the symbol it names -> --around=SYM";; *) no "perf-symbol route wrong: $PS";; esac +PS0="$( route 'the profiler vendor is offering licenses, should we buy a few seats?' )" +case "$PS0" in *'--around='*) no "profile wording with no resolved symbol invented an --around: $PS0";; *) ok "profile wording alone (no symbol) abstains rather than invent one";; esac +GQ="$( route 'which functions can reach targetSymbol - one-hop callers cannot phrase that' )" +case "$GQ" in *'status="recommend"'*'intent="graph-query"'*'skill="ripwire-graph-query"'*'--graph-query='*'targetSymbol'*) ok "a bounded-closure question + one symbol -> --graph-query=EXPR";; *) no "graph-query route wrong: $GQ";; esac +MR="$( route 'where is the rot in code I did not write' )" +case "$MR" in *'status="recommend"'*'intent="maintenance-risk"'*'skill="ripwire-fresh-eyes"'*'--hotspots'*) ok "maintenance-risk wording -> --hotspots";; *) no "maintenance-risk route wrong: $MR";; esac +OR="$( route 'clang says the inner loop was not vectorized - is that worth a diff here?' )" +case "$OR" in *'status="recommend"'*'intent="opt-remark"'*'skill="ripwire-opt-remarks"'*'--for='*) ok "a clang optimization remark -> the ranked lens, under the opt-remarks skill";; *) no "opt-remark route wrong: $OR";; esac +# Execution check: the two catalog commands that carry a COMPOSED value are not placeholders. Unquote +# what the router emitted and run it through the real verb, the same way the SYM:VAR arm above does. +GQEXPR="$( printf '%s' "$GQ" | sed -n 's|.*--graph-query='\(.*\)'.*|\1|p' | sed 's/"/"/g' )" +GQRUN="$( "$BIN" "$REPO" --no-cache --graph-query="$GQEXPR" )"; rc=$? +{ [ $rc -eq 0 ] && printf '%s' "$GQRUN" | grep -q '"$REPO/PLAN_GATE.md" +PLRUN="$( "$BIN" "$REPO" --no-cache --plan-lint=PLAN_GATE.md )"; rc=$? +[ $rc -le 2 ] && ok "the emitted --plan-lint=FILE command runs against a real plan file (rc=$rc)" \ + || no "the emitted --plan-lint=FILE command failed to run (rc=$rc)" +rm -f "$REPO/PLAN_GATE.md" +# ── two routers, ONE vocabulary: every shipped skill must be nameable by --help-task ────────────────── +# F-R1-09 measured 8 of 16. This arm reads BOTH sides from disk — the skill directories that exist, and +# the skill= names src/taskroute.h can emit — so it fails when a NEW skill ships with no route as much as +# when a route names a skill that does not exist. ripwire-router is excluded: it is the fallback map, not +# a destination (test/skillevalcheck.sh refuses it as a label for the same reason). +routerNames="$( grep -o 'ripwire-[a-z-]*' "$ROOT/src/taskroute.h" | sort -u )" +unnameable=""; phantom="" +for _d in "$ROOT"/skills/*/; do + _s="$( basename "$_d" )" + [ -f "$_d/SKILL.md" ] || continue + [ "$_s" = "ripwire-router" ] && continue + printf '%s\n' "$routerNames" | grep -qx "$_s" || unnameable="$unnameable $_s" +done +for _n in $routerNames; do + [ -f "$ROOT/skills/$_n/SKILL.md" ] || phantom="$phantom $_n" +done +[ -z "$unnameable" ] && ok "every shipped skill (except ripwire-router) is nameable by --help-task" \ + || no "shipped skill(s) no --help-task answer can ever name:$unnameable" +[ -z "$phantom" ] && ok "every skill the router can name exists on disk" \ + || no "router names skill(s) with no directory:$phantom" + N="$( route 'Write a cheerful release announcement' )" case "$N" in *'status="abstain"'*) ok "off-topic prompt abstains";; *) no "off-topic prompt did not abstain: $N";; esac [ "$( printf '%s' "$N" | grep -o '' | wc -l | tr -d ' ' )" = 0 ] && ok "abstention emits zero commands" || no "abstention emitted a command" diff --git a/test/taskroutefix/PROVENANCE.md b/test/taskroutefix/PROVENANCE.md index a9d32256b..6eddcb519 100644 --- a/test/taskroutefix/PROVENANCE.md +++ b/test/taskroutefix/PROVENANCE.md @@ -229,3 +229,66 @@ its route. **Seal: sha256(prompts.tsv) = `25283f2eba85aad889fe3746308df76ed8b1244529f44986c936eb6ef60b0b53`** (post-round; rows=189, dev=100, test=89). + +## Catalog-tier round (2026-09-10, lane/helptask-precision) — the verbs and skills with no route + +**The gap.** The audit measured `--help-task` at **3 recommends over 39 phrasings** of the 13 surfaces +added since 2026-08-28 (F-R1-08), and found the router able to name **8 of the 16** shipped skills +(F-R1-09) — `--help-task` and the skill catalog were two routers with two vocabularies. Three of the +unrouted surfaces are VERBS, not shaping flags: `--handoff` (which has its own shipped skill), +`--plan-lint`, and the PROSE form of `--from-trace` (`looksLikeTrace` matches a PASTED artifact, and a +sanitizer report described in words contains none of its literals). + +**Ten new intents** in a `catalogTaskChoice` tier that sits LAST in `directTaskChoice`, so every older +and more specific route keeps its rows: `handoff-brief` (`--handoff`), `plan-lint` (`--plan-lint=FILE`), +`trace-prose` (`--from-trace=-`), `scan-skills`/`scan-skill` (`--scan-skills`, `--scan-skill=FILE`), +`opt-remark` (`--for=TASK`), `architecture-health` (`--deps`), `quality-check` (`--quality-delta`), +`perf-symbol` (`--around=SYM`), `graph-query` (`--graph-query=EXPR`), `maintenance-risk` +(`--hotspots`). Skills nameable: **8 → 16**, and `test/taskroutecheck.sh` now reads BOTH sides from disk +so a new skill shipping without a route fails as loudly as a route naming a skill that does not exist. + +**Rows added: 36 (30 positives, 3 per intent, + 6 negatives), `provenance=instrumented-cli`**, split by +the same content-hash rule. `instrumented-cli` for the same reason the 2026-09-02 section gives: each new +intent's trigger is a small closed phrase list, so a sentence that routes necessarily reuses one of its +phrases. Every row's routing outcome was verified against a live binary before insertion (30/30 after one +correction — see below); the 6 negatives are the near misses that must NOT route (an account handed off +to support, a landing-page design that needs a check, vetting a candidate's onboarding plan, a team that +inherited a support queue, a profiler vendor selling licences, a quarterly summary handed to leadership). + +**Two corrections the pre-insertion verification caught, recorded rather than smoothed over:** + +- *"lint the shape of DESIGN_NOTES.md before I circulate it"* abstained: the surface test wanted the + words "plan"/"design doc" in the PROSE. A file that names ITSELF a plan (`PLAN_*.md`, `DESIGN_*.md`) is + surface evidence the prose need not repeat, so the check now reads the named file's own name too. +- *"lint the plan file layout before I commit it"* routed to `quality-check`. `"before i commit"` is a + TIMING word, not a quality word — it fits linting a plan or running a gate equally well. Re-weighted + below the floor so it can only ever CONFIRM a quality word, never carry the route alone. The prompt + now abstains, which is correct: it names no file, and `--plan-lint` refuses a file that is not there. + +**Held-out floors, before → after** (`bench/taskroute_eval.py`, same corpus, only the binary changed — +the pre-change binary is this lane's own commit 2, built and kept for the comparison): + +| split | rows | accuracy | precision | harmful | neg-specificity | coverage | +| --- | ---: | ---: | ---: | ---: | ---: | ---: | +| test | 114 | 0.754 → **0.939** | 1.000 → 1.000 | 0.000 → 0.000 | 1.000 → 1.000 | 0.627 → **0.907** | +| dev | 111 | — → **0.946** | — → 1.000 | — → 0.000 | — → 1.000 | — → **0.929** | +| all | 225 | 0.809 → **0.942** | 1.000 → 1.000 | 0.000 → 0.000 | 1.000 → 1.000 | 0.730 → **0.918** | + +This round's red-first proof is the GATE, not the eval: coverage has no floor by the round-1 rule, so +the eval exits 0 either way. Eleven `taskroutecheck` arms fail against the pre-change binary (every one +abstained with `score="0"`), plus the two execution arms; the skill-vocabulary arm fails against the +pre-change SOURCE, naming all eight skills no `--help-task` answer could reach. + +**Regression discipline.** All 189 rows that predate this tier are BYTE-IDENTICAL on +(status, intent, resolved_symbols) between this lane's commit 2 and commit 3. Surface coverage on the +audit's own 39 phrasings: **3/39 → 9/39** — the remaining 30 are the shaping flags (`--scope`, +`--slice-depth`, `--slice-flow`, `--allow-dirty`, `--no-ignore`, `--no-post-check`), the eval-only +`--pin-census`, `--edit-check` paging, and value-carrying abstentions, all of which a one-command router +declines by design. + +**Screen: unchanged at 2 flagged lines** (line 61 pre-existing, line 176 from the previous section) even +though this round added a large amount of new card vocabulary to `src/taskroute.h` — no `handwritten*` +row collides with any of it. + +**Seal: sha256(prompts.tsv) = `1719aea95449e222718ec38151d2bd6998a95e1dd070038baa0b6e28fd0c9cf5`** +(post-round; rows=225, dev=111, test=114). diff --git a/test/taskroutefix/prompts.tsv b/test/taskroutefix/prompts.tsv index 160b6226b..fc4f1fff1 100644 --- a/test/taskroutefix/prompts.tsv +++ b/test/taskroutefix/prompts.tsv @@ -188,3 +188,39 @@ test clean abstain instrumented-cli walk me through the implementation of author test clean understand-symbol instrumented-cli walk me through the implementation of prefix before i touch it test clean understand-symbol instrumented-cli i want to understand the implementation of audit end to end test clean understand-symbol instrumented-cli how does classify work? show me the body of classify +dev clean handoff-brief instrumented-cli I am going on leave next week - put together a brief on the scheduler for whoever takes it over +test clean handoff-brief instrumented-cli hand this area off to the next session with the entry points and the risky bits +test clean handoff-brief instrumented-cli my teammate is picking this up tomorrow, write the handover for the ingest path +test clean plan-lint instrumented-cli does PLAN_CACHE_ROUND.md have the structure a plan file is supposed to have? +dev clean plan-lint instrumented-cli lint the shape of DESIGN_NOTES.md before I circulate it +test clean plan-lint instrumented-cli check that docs/PLAN_NEXT.md is well-formed as a plan document +dev clean trace-prose instrumented-cli I have a sanitizer report from last night - map it onto the indexed symbols +test clean trace-prose instrumented-cli here is a compiler error from the build, which of my symbols does it name +test clean trace-prose instrumented-cli got a crash log with frames in it, translate it into symbols I can read +test clean scan-skills instrumented-cli someone sent me a skills bundle - is it safe to install, any prompt injection in there? +dev clean scan-skills instrumented-cli vet this untrusted skill collection before installing it +test clean scan-skill instrumented-cli check tools/helper.md for exfiltration before installing it as a skill +test clean opt-remark instrumented-cli clang says the inner loop was not vectorized - is that worth a diff here? +dev clean opt-remark instrumented-cli the optimization remark claims this will not be inlined, where does that land +test clean opt-remark instrumented-cli I am reading an opt-record dump and want the code behind one of the remarks +test clean architecture-health instrumented-cli do we have a dependency mess in here - any circular dependencies or god file? +test clean architecture-health instrumented-cli are there layering violations we should be gating on +dev clean architecture-health instrumented-cli what is the propagation cost of touching this module +test clean quality-check instrumented-cli before I call it done - did my change make anything worse? +test clean quality-check instrumented-cli I want to verify the cleanup actually removed debt rather than adding new debt +test clean quality-check instrumented-cli is the code I just wrote past the quality bar +dev clean perf-symbol instrumented-cli the profiler puts targetSymbol at the top - what is around it +test clean perf-symbol instrumented-cli a perf record run names parseConfigValue as the hot symbol, show me its neighborhood +test clean perf-symbol instrumented-cli flame graph says renderXmlRow is the hottest frame here +test clean graph-query instrumented-cli which functions can reach cacheValue - one-hop callers cannot phrase that +dev clean graph-query instrumented-cli everything that reaches StorageDriver transitively, please +test clean graph-query instrumented-cli I need a graph query for what sits within one hop of sendRequest +test clean maintenance-risk instrumented-cli I inherited this service and have no idea what is gnarly in it +test clean maintenance-risk instrumented-cli where is the rot in code I did not write +dev clean maintenance-risk instrumented-cli is this module safe to touch, what is the maintenance risk +dev clean abstain instrumented-cli we handed the account off to support last week, any update on the customer? +test clean abstain instrumented-cli the design of the new landing page needs a check before Friday +test clean abstain instrumented-cli vet the candidate's onboarding plan with HR before the offer goes out +test clean abstain instrumented-cli our team inherited the support queue from the old vendor +dev clean abstain instrumented-cli the profiler vendor is offering licenses, should we buy a few seats? +test clean abstain instrumented-cli write the quarterly summary for leadership and hand me a draft From 05a800bd4ac0c3366ff89fafc74cbf1dcf9ce94a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:56:48 -0400 Subject: [PATCH 20/73] perf(lint,match): the #match? regex compiled once per query, not once per match MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `passesPredicates` ran `std::regex_search( lhs, std::regex( rhs ) )` once per query MATCH, per file (src/ingest_astquery.h, called from astQueryGrouped's two exec loops). `rhs` for a String-typed argument is a CONSTANT owned by the TSQuery — `ts_query_string_value_for_id` hands back the same bytes every time — so the most expensive constructor in the standard library was answering a question whose answer never changes. MEASURED BEFORE (1 ms `sample`, `--lint` over the go corpus, 44,376 busy leaf samples): the `std::basic_regex` subtree is 13.52% of busy CPU and 100% of it is owned by passesPredicates; the whole predicate evaluator is 20.65%. The leaves are CONSTRUCTION, not matching — `__parse_ERE_dupl_symbol` 253, `__parse_atom` 209, the `__state` vector's growth 178+174, against `__match_at_start_ecma` 273 — and regex construction is ~46.7% of every malloc leaf in the run. PredicateRegexTable holds one compiled `std::regex` per ( query, string id ), built by buildPredicateRegexTable when the query is compiled and hung off GroupedQuery / GrammarQueries beside the TSQuery it belongs to. `value_id` indexes the query's own string table, so `ts_query_string_count` sizes an exact O(1) lookup: no hashing, no comparison, no allocation left on the per-match path. THREE STATES, and the middle one is the contract: slot >= 0 is the precompiled constant; slot == -2 is a constant `std::regex` REFUSED, which must filter NOTHING — exactly what the old per-match `catch( ... ) { ok = true; }` did; slot == -1 is a Capture-typed argument, whose pattern is per-match text and stays dynamic. Getting -2 wrong would turn a broken rule from one that keeps every row into one that drops every row, which is why it is its own gate arm. A/B — CPU (user+sys via rusage), 12 interleaved runs per arm with the arms swapped at the half, one unrecorded warm-up per arm: | corpus | argv | A median | B median | delta med | delta min | B.med 18.074 s (-0.92%, inside the noise at load 40). The A/B beats the audit round's own -5.93% estimate because that arm memoised inside passesPredicates; this one precompiles every predicate of the COMBINED per-grammar query at compile time, which is the query the workers actually run. BYTE-IDENTICAL, 21 of 21 arms against the binary this lane started from (i.e. both of the lane's commits together), three corpora — go, rocksdb, a pristine ripwire tree — over --lint, --lint-select, --match (plain, #match? and #not-match?), the default map --top-k=100000, --for, --pack-task, --grep, --clones, --hotspots and --slice. Determinism: two runs byte-identical; `xmllint --noout` clean. GATE FIRST — test/astqueryregexcheck.sh + test/astqueryregex_golden.txt, the golden RECORDED FROM THE PRE-CHANGE BINARY over a fixture the script materialises itself (so the corpus cannot drift out from under the golden): A --lint + 7 --match probes byte-identical to that golden (14,915 B). B non-vacuity: the two #match?-only lint rules are live on the fixture (7 and 2 findings), so arm A is not a golden of an empty filter. C1-C4 the four semantics a "compile it once" change can silently move, each as a differential probe: case sensitivity, regex_search vs regex_match, #not-match? as the exact complement, and the malformed-pattern refusal filtering nothing. D a Capture-typed #match? argument still evaluates per match. E mutation control for C: all four expectations, inverted, are false — so no arm in C is comparing a value with itself. (This arm caught a real defect while the gate was being written: the map carries no trailing newline, so a blank-line section delimiter did not exist and one differential arm was reading the whole file on both sides.) F determinism under the query pool: 5 --lint runs byte-identical. The compiled regex is now SHARED read-only across workers instead of built per match. THE REBUILD MUTATION, run once by hand rather than in the gate (a gate that rebuilds the binary is super-linear in CI contention): a scratch build with `std::regex::icase` added to the precompiled construction turns arm A red and both C1 arms red — "^Foo" and "^foo" each return 2 hits instead of 1, and select the same two functions. Reverted and rebuilt before landing. SANITIZERS on the changed paths: ASan+UBSan+LSan (`-fno-sanitize-recover=all`, committed lsan_suppressions.txt) clean over the map, --lint, --match with a valid and a MALFORMED #match? pattern, and --slice. ThreadSanitizer clean over --lint on the repo, --lint on the multi-language test fixtures, and --match with a #match? predicate — the empirical half of the [res.on.data.races] claim that a const std::regex may be shared across the worker pool. --quality-delta: 5 gating rows, 4 of them short-horizon-churn on the one file this touches, and one api-surface contract-change — passesPredicates taking the table, 3 params to 4. The first draft also scored complexity 29 -> 48 and verbosity 70 -> 84 on passesPredicates; the three-state decision moved into evalMatchPredicate and both regressions are gone. --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/ingest_astquery.h | 152 +++++++++++++++++++++-- test/astqueryregex_golden.txt | 24 ++++ test/astqueryregexcheck.sh | 212 +++++++++++++++++++++++++++++++++ test/regression.sh | 2 +- 7 files changed, 388 insertions(+), 18 deletions(-) create mode 100644 test/astqueryregex_golden.txt create mode 100755 test/astqueryregexcheck.sh diff --git a/README.md b/README.md index d157ea9f3..b72eb8f4f 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-587 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +588 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 **587 gate scripts** and is the authoritative list; +`test/regression.sh` names **588 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b1058118..6144576de 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` | 587 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 588 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) @@ -5625,7 +5625,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 **587 gate scripts**, all of which exist on disk. +naming **588 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`. @@ -6637,7 +6637,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 587. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 588. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index fc5a0220f..606d106f2 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["587 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 + ["588 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "587", "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, "588", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["588 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/ingest_astquery.h b/src/ingest_astquery.h index 00440c8a8..7a051c81e 100644 --- a/src/ingest_astquery.h +++ b/src/ingest_astquery.h @@ -36,9 +36,138 @@ inline bool captureText( const TSQueryMatch& m, std::uint32_t capIndex, std::str return false; } +// ---- the compiled #match? / #not-match? regexes of ONE query, resolved when the query is ---------- +// One `std::regex` per ( query, string id ), built when the query is compiled instead of once per MATCH. +// +// WHY. `passesPredicates` ran `std::regex_search( lhs, std::regex( rhs ) )` per match, per file, and `rhs` +// is a CONSTANT owned by the TSQuery — `ts_query_string_value_for_id` hands back the same bytes every time. +// So the most expensive constructor in the standard library was answering a question whose answer never +// changes. A 1 ms `sample` of `--lint` over the go corpus (44,376 busy leaf samples) put the +// `std::basic_regex` subtree at 13.52% of busy CPU with 100% of it owned by passesPredicates, and regex +// CONSTRUCTION — `__parse_ERE_dupl_symbol`, `__parse_atom`, the `__state` vector's growth — at ~46.7% of +// every malloc leaf in the run. Whole-query regex counts are single digits; the table is tiny. +// +// KEYED BY STRING ID, NOT BY TEXT. `value_id` indexes the query's own string table, so +// `ts_query_string_count` sizes an exact O(1) lookup and no hashing, no comparison and no allocation is +// left on the per-match path. -1 = this string is not a precompilable regex argument; -2 = it IS one and +// `std::regex` REFUSED it. +// +// THE REFUSAL IS PART OF THE CONTRACT. A malformed pattern used to throw out of the per-match constructor, +// get caught, and leave `ok = true` — i.e. filter NOTHING. Precompiling moves that throw from the match to +// the build, so -2 exists to reproduce it exactly; without it a broken rule would silently drop every row +// instead of silently keeping them. test/astqueryregexcheck.sh arm C4. +// +// A CAPTURE-TYPED ARGUMENT IS NEVER IN HERE. `(#match? @a @b)`'s pattern is the matched node's own text — +// per match by construction — and stays dynamic (arm D). +// +// THREADING. Built single-threaded with the query, then SHARED by every worker that evaluates predicates. +// Concurrent use of `const` standard-library operations is data-race-free ([res.on.data.races]), and +// `std::regex_search`'s state lives in the algorithm, not in the pattern; arm F is the empirical half. +struct PredicateRegexTable +{ + std::vector slotOfStringId; // per query-string id: -1 not precompiled, -2 refused, >=0 index into res + std::vector res; +}; + +// Walk every predicate of every pattern once and compile the constant #match?/#not-match? arguments. +// Deliberately a mirror of passesPredicates' own step-group loop below — the two must agree about which +// argument is the pattern, and a second spelling of that rule is how a filter quietly changes meaning. +inline PredicateRegexTable buildPredicateRegexTable( const TSQuery* q ) +{ + PredicateRegexTable table; + if( q == nullptr ) + { + return table; + } + table.slotOfStringId.assign( ts_query_string_count( q ), -1 ); + const std::uint32_t patterns = ts_query_pattern_count( q ); + for( std::uint32_t patternIndex = 0; patternIndex < patterns; ++patternIndex ) + { + std::uint32_t pc = 0; + const TSQueryPredicateStep* steps = ts_query_predicates_for_pattern( q, patternIndex, &pc ); + for( std::uint32_t i = 0; i < pc; ) + { + const std::uint32_t begin = i; + for( ; i < pc && steps[i].type != TSQueryPredicateStepTypeDone; ++i ) + { + } + const std::uint32_t n = i - begin; + ++i; // skip the Done step + if( n < 3 || steps[begin].type != TSQueryPredicateStepTypeString ) + { + continue; + } + std::uint32_t nl = 0; // two statements — see the sequencing note in passesPredicates + const char* opText = ts_query_string_value_for_id( q, steps[begin].value_id, &nl ); + if( opText == nullptr ) + { + continue; + } + const std::string_view op( opText, nl ); + if( op != "match?" && op != "not-match?" ) + { + continue; + } + const TSQueryPredicateStep& arg = steps[ begin + 2 ]; + if( arg.type != TSQueryPredicateStepTypeString || arg.value_id >= table.slotOfStringId.size() + || table.slotOfStringId[ arg.value_id ] != -1 ) + { + continue; // capture-typed, out of range, or already decided + } + std::uint32_t rl = 0; + const char* rv = ts_query_string_value_for_id( q, arg.value_id, &rl ); + if( rv == nullptr ) + { + continue; + } + try + { + table.res.emplace_back( std::string( rv, rl ) ); // same construction, same default ECMAScript flags + table.slotOfStringId[ arg.value_id ] = static_cast( table.res.size() - 1 ); + } + catch( ... ) + { + table.slotOfStringId[ arg.value_id ] = -2; // refused — reproduce the old per-match catch + } + } + } + return table; +} + +// ONE #match? / #not-match? predicate, decided. Its own function rather than an inline block because the +// three states below cost passesPredicates +19 cognitive complexity inline, on a function already well over +// the ccx bar — and because the states are the whole contract of the precompile and deserve to be read in +// one place: +// * slot >= 0 — a precompiled constant pattern. The common case, and the point of the table. +// * slot == -2 — a constant pattern std::regex REFUSED. Filter NOTHING, which is exactly what the old +// per-match `catch( ... ) { ok = true; }` did when the same construction threw at the same pattern. +// * slot == -1 — no constant to precompile (a Capture-typed argument, whose pattern is per-match text). +// Construct it here, per match, as before. +// `rhs` is only read on the last of the three; it is the caller's already-materialised argument text. +inline bool evalMatchPredicate( bool negated, const std::string& lhs, const std::string& rhs, + const PredicateRegexTable& rx, const TSQueryPredicateStep& arg ) +{ + const bool constant = ( arg.type == TSQueryPredicateStepTypeString && arg.value_id < rx.slotOfStringId.size() ); + const std::int32_t slot = constant ? rx.slotOfStringId[ arg.value_id ] : -1; + if( slot == -2 ) + { + return true; // the pattern did not compile ⇒ this predicate filters nothing + } + try + { + const bool mm = ( slot >= 0 ) ? std::regex_search( lhs, rx.res[ std::size_t( slot ) ] ) + : std::regex_search( lhs, std::regex( rhs ) ); + return negated ? !mm : mm; + } + catch( ... ) + { + return true; // same arm the per-match construction always took + } +} + // evaluate a pattern's query predicates against a match — #eq? / #not-eq? (string/capture equality) and // #match? / #not-match? (ECMAScript regex). ts_query never applies these itself; without this, #eq? is a no-op. -inline bool passesPredicates( const TSQuery* q, const TSQueryMatch& m, std::string_view src ) +inline bool passesPredicates( const TSQuery* q, const PredicateRegexTable& rx, const TSQueryMatch& m, std::string_view src ) { std::uint32_t pc = 0; const TSQueryPredicateStep* steps = ts_query_predicates_for_pattern( q, m.pattern_index, &pc ); @@ -100,7 +229,9 @@ inline bool passesPredicates( const TSQuery* q, const TSQueryMatch& m, std::stri ok = ( lhs != rhs ); } else if( op == "match?" || op == "not-match?" ) - { try { const bool mm = std::regex_search( lhs, std::regex( rhs ) ); ok = ( op == "match?" ) ? mm : !mm; } catch( ... ) { ok = true; } } + { + ok = evalMatchPredicate( op == "not-match?", lhs, rhs, rx, pr[2] ); + } if( !ok ) { return false; @@ -175,9 +306,10 @@ inline AstMatch makeAstMatch( std::uint32_t fileId, std::string_view bytes, cons // a worker executes every query a file's grammar has and files the captures into that query's own bucket. struct GroupedQuery { - TSQuery* query = nullptr; - std::string tag; - std::uint32_t groupIndex = 0; + TSQuery* query = nullptr; + std::string tag; + std::uint32_t groupIndex = 0; + PredicateRegexTable rx; // this query's compiled #match? patterns — built with the query, read per match }; // Every query one grammar has to answer, in BOTH shapes. `perSpec` is one compiled query per spec, the @@ -196,6 +328,7 @@ struct GrammarQueries { std::vector perSpec; TSQuery* combined = nullptr; // nullptr = degraded to one tree walk per spec + PredicateRegexTable combinedRx; // the combined query's own compiled #match? patterns std::vector patternOwner; // combined pattern index -> index into perSpec }; @@ -225,7 +358,7 @@ GrammarQueries compileGrammarQueries( const TSLanguage* g, const std::vector( gqs.perSpec.size() ) ); } - gqs.perSpec.push_back( { q, spec.tag, static_cast( groupIndex ) } ); + gqs.perSpec.push_back( { q, spec.tag, static_cast( groupIndex ), buildPredicateRegexTable( q ) } ); combinedSrc.append( spec.query ); combinedSrc.push_back( '\n' ); // a spec may end in a `;` line comment; never let it swallow the next } @@ -236,7 +369,8 @@ GrammarQueries compileGrammarQueries( const TSLanguage* g, const std::vector( combinedSrc.size() ), &off, &err ); if( comb != nullptr && ts_query_pattern_count( comb ) == static_cast( gqs.patternOwner.size() ) ) { - gqs.combined = comb; + gqs.combined = comb; + gqs.combinedRx = buildPredicateRegexTable( comb ); } else { @@ -902,7 +1036,7 @@ std::vector> astQueryGrouped( const IngestResult& ing, con TSQueryMatch m; while( ts_query_cursor_next_match( cur, &m ) ) { - if( !passesPredicates( q, m, bytes ) ) + if( !passesPredicates( q, it->second.combinedRx, m, bytes ) ) { continue; // honour #eq? / #match? etc. — predicates are per PATTERN, so this reads the right ones } @@ -921,7 +1055,7 @@ std::vector> astQueryGrouped( const IngestResult& ing, con TSQueryMatch m; while( ts_query_cursor_next_match( cur, &m ) ) { - if( !passesPredicates( gq.query, m, bytes ) ) + if( !passesPredicates( gq.query, gq.rx, m, bytes ) ) { continue; // honour #eq? / #match? etc. } diff --git a/test/astqueryregex_golden.txt b/test/astqueryregex_golden.txt new file mode 100644 index 000000000..3fb29376f --- /dev/null +++ b/test/astqueryregex_golden.txt @@ -0,0 +1,24 @@ +===== lint +strcpysprintfstrcatMD5strcpyBarrier ~ barrier2 (edit distance <=2)sprintfmd4strcatgets +----- end lint +===== match-anchor-Foo +Foo +----- end match-anchor-Foo +===== match-anchor-foo +foo2 +----- end match-anchor-foo +===== match-substring +Foofoo2 +----- end match-substring +===== match-not-anchor +foo2useHashstrcpBarrierbarrier2useMd4plainCalsoPlain +----- end match-not-anchor +===== match-malformed +Foofoo2useHashstrcpBarrierbarrier2useMd4plainCalsoPlain +----- end match-malformed +===== match-no-predicate +Foofoo2useHashstrcpBarrierbarrier2useMd4plainCalsoPlain +----- end match-no-predicate +===== match-capture-arg +strcpysstrcats +----- end match-capture-arg diff --git a/test/astqueryregexcheck.sh b/test/astqueryregexcheck.sh new file mode 100755 index 000000000..0f4a3cb71 --- /dev/null +++ b/test/astqueryregexcheck.sh @@ -0,0 +1,212 @@ +#!/usr/bin/env bash +# astqueryregexcheck.sh — gate for the #match? / #not-match? predicate regex in passesPredicates +# (src/ingest_astquery.h): its COMPILATION was hoisted out of the per-match path into a table built once +# per compiled TSQuery, and none of its SEMANTICS may move with it. +# +# WHY THIS EXISTS. `std::regex_search( lhs, std::regex( rhs ) )` ran once per query MATCH, per file, and +# `rhs` is a constant owned by the TSQuery — so the most expensive constructor in the standard library was +# being run to answer a question whose answer never changes. A 1 ms `sample` of `--lint` over the go +# corpus (44,376 busy leaf samples) put the `std::basic_regex` subtree at 13.52% of busy CPU, 100% of it +# owned by passesPredicates, and regex CONSTRUCTION (not matching) at ~46.7% of every malloc leaf in the +# run. Hoisting it is worth doing and is exactly the kind of change that silently alters a filter. +# +# WHAT IS AT RISK, and therefore what is gated. Five things a "compile it once" change can quietly move: +# * the FLAGS. std::regex's default is ECMAScript, case-SENSITIVE. A build that added icase would still +# look right on most patterns. +# * search vs match. regex_search finds a substring; regex_match requires the whole string. +# * the REFUSAL. A malformed pattern threw out of the constructor, was caught, and left `ok = true` — +# i.e. it filtered NOTHING. Precompiling moves that throw from the match to the build, so the arm has +# to be preserved deliberately; getting it wrong turns a broken rule into a rule that drops every row. +# * the CAPTURE-typed argument. `(#match? @a @b)` has no constant to precompile — it must stay dynamic. +# * THREAD SAFETY. The compiled regex is now SHARED across the query pool's workers instead of being +# built per match. Concurrent const use of a standard library object is data-race-free by +# [res.on.data.races], so this is legal — arm F is the empirical half of that claim. +# +# ARMS +# A GOLDEN. `--lint` and six `--match` probes over a fixture this script materialises (deterministic +# text, written here, so the corpus cannot drift out from under the golden) must be byte-identical to +# test/astqueryregex_golden.txt, which was RECORDED FROM THE PRE-CHANGE BINARY. That is the whole +# "the hoist changed nothing" claim, in the only form that can be checked later by someone who was +# not there. UPDATE_GOLDEN=1 re-records it — review the diff first. +# B NON-VACUITY. The golden must contain the rows the predicates actually decide (a #match?-only lint +# rule with a non-zero count), or arm A would be a golden of an empty filter. +# C CAN GO RED WITHOUT A REBUILD. Four differential probes whose ANSWERS pin the semantics above, so a +# build that moved any of them fails arm A AND is diagnosed by name here: +# C1 case sensitivity — "^Foo" and "^foo" must select DIFFERENT single functions (an icase build +# makes both select two, which is the mutation the round actually ran; see the note below). +# C2 search, not match — a bare substring "oo" must select both of them. +# C3 complement — #not-match? on a pattern must select exactly the rows #match? does not. +# C4 refusal — a malformed pattern "(" must filter NOTHING, i.e. return the same rows as +# the same query with no predicate at all. +# D CAPTURE-TYPED ARGUMENT. `(#match? @a @b)` still evaluates per match (its pattern is not a constant), +# and the probe's answer is pinned in the golden with the rest. +# E MUTATION CONTROL for arm C. Each of C1-C4 is re-run with its expectation INVERTED and must fail — +# an arm that cannot be observed failing is decoration. +# F DETERMINISM UNDER THE POOL. `--lint` is run five times over a multi-file fixture; all five must be +# byte-identical. The regex is shared read-only across the worker threads that evaluate predicates, +# and this is the arm that would catch a shared-mutable-state regression in the cheapest place. +# +# THE REBUILD MUTATION, run once by hand rather than in this gate. The round that landed the hoist built a +# scratch binary with `std::regex::icase` added to the precompiled construction and confirmed arm C1 goes +# red (both probes return 2 hits instead of 1 each) and arm A fails. It is NOT run here: a gate that builds +# the whole binary is super-linear in CI contention (see the round notes on crossdirincludecheck), and +# C1's differential answer is the same evidence at a thousandth of the cost. +# +# Usage: bash test/astqueryregexcheck.sh [ RIPWIRE_BIN=build/ripwire ] [ UPDATE_GOLDEN=1 ] +# Exits non-zero on any failure. Does NOT edit regression.sh. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${RIPWIRE_BIN:-$ROOT/build/ripwire}" +case "$BIN" in /*) ;; *) BIN="$ROOT/$BIN";; esac +GOLD="$ROOT/test/astqueryregex_golden.txt" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +echo "astqueryregexcheck: BIN=$BIN" + +# ── the fixture, written HERE so the corpus can never drift out from under the golden ──────────────── +FIX="$TMP/fix" +mkdir -p "$FIX" +cat > "$FIX/a.cpp" <<'FIXA' +#include +void Foo( char* dst, const char* srcText ) +{ + strcpy( dst, srcText ); + sprintf( dst, "%s", srcText ); +} +void foo2( char* d ) { strcat( d, "x" ); } +unsigned MD5( const char* p ); +unsigned sha1sum( const char* p ); +void useHash( const char* p ) { MD5( p ); sha1sum( p ); } +int strcp( int x ) { return x; } +FIXA +cat > "$FIX/b.cpp" <<'FIXB' +#include +namespace second +{ +void Barrier( char* d, const char* s ) { strcpy( d, s ); } +void barrier2( char* d ) { sprintf( d, "%d", 1 ); } +unsigned md4( const char* p ); +void useMd4( const char* p ) { md4( p ); } +} +FIXB +cat > "$FIX/c.c" <<'FIXC' +#include +void plainC( char* d, const char* s ) { strcat( d, s ); } +void alsoPlain( char* d ) { gets( d ); } +FIXC + +# Every probe, run through ONE helper so the golden and the differential arms cannot disagree about how a +# run is spelled. The corpus root is an absolute temp path, so it is normalised out; nothing else in these +# outputs is machine-dependent (paths inside the map are already root-relative). +FN_DEF='(function_definition declarator: (function_declarator declarator: (identifier) @n)' +probe(){ # $1 = label, rest = argv after the corpus + local label="$1"; shift + printf '===== %s\n' "$label" + "$BIN" "$FIX" --no-cache "$@" 2>/dev/null | sed "s#$FIX##g" + # An EXPLICIT terminator, not a blank line: the map output carries no trailing newline (G4), so a + # blank-line delimiter would not exist and every per-section `sed` range below would silently run to + # end of file — which is exactly the shape of a differential arm that compares two identical + # whole-file reads and reports "different" forever. + printf '\n----- end %s\n' "$label" +} +emit_all(){ + probe "lint" --lint + probe "match-anchor-Foo" "--match=$FN_DEF (#match? @n \"^Foo\"))" + probe "match-anchor-foo" "--match=$FN_DEF (#match? @n \"^foo\"))" + probe "match-substring" "--match=$FN_DEF (#match? @n \"oo\"))" + probe "match-not-anchor" "--match=$FN_DEF (#not-match? @n \"^Foo\"))" + probe "match-malformed" "--match=$FN_DEF (#match? @n \"(\"))" + probe "match-no-predicate" "--match=$FN_DEF)" + probe "match-capture-arg" "--match=(call_expression function: (identifier) @f arguments: (argument_list (identifier) @a) (#match? @f @a))" +} + +emit_all > "$TMP/now.txt" + +# ── A. the golden ──────────────────────────────────────────────────────────────────────────────────── +if [ "${UPDATE_GOLDEN:-0}" = "1" ]; then + cp "$TMP/now.txt" "$GOLD" + ok "A: UPDATE_GOLDEN=1 — re-recorded $( wc -c < "$GOLD" | tr -d ' ' ) B into test/astqueryregex_golden.txt (review the diff)" +elif [ ! -f "$GOLD" ]; then + no "A: no golden at test/astqueryregex_golden.txt — record it with UPDATE_GOLDEN=1 against the PRE-change binary" +elif cmp -s "$TMP/now.txt" "$GOLD"; then + ok "A: --lint + 7 --match probes byte-identical to the recorded golden ($( wc -c < "$GOLD" | tr -d ' ' ) B)" +else + no "A: output differs from test/astqueryregex_golden.txt" + diff "$GOLD" "$TMP/now.txt" | head -20 | sed 's/^/ /' +fi + +# ── B. non-vacuity: the predicates must actually be deciding something ─────────────────────────────── +UNSAFE="$( sed -n 's/.*rule name="unsafe-c-fn" count="\([0-9]*\)".*/\1/p' "$TMP/now.txt" | head -1 )" +WEAK="$( sed -n 's/.*rule name="weak-crypto" count="\([0-9]*\)".*/\1/p' "$TMP/now.txt" | head -1 )" +if [ "${UNSAFE:-0}" -lt 3 ] || [ "${WEAK:-0}" -lt 1 ]; then + no "B: the two #match?-only lint rules found ${UNSAFE:-0} / ${WEAK:-0} — the golden would be a golden of an empty filter" +else + ok "B: the #match?-only lint rules are live on this fixture (unsafe-c-fn=$UNSAFE, weak-crypto=$WEAK)" +fi + +# ── C. differential arms — each one pins a semantic the hoist could have moved ─────────────────────── +section(){ sed -n "/^===== $1\$/,/^----- end $1\$/p" "$TMP/now.txt"; } +hits(){ section "$1" | sed -n 's/.*/dev/null > "$TMP/det0" +for i in 1 2 3 4; do + "$BIN" "$FIX" --no-cache --lint 2>/dev/null > "$TMP/det$i" + cmp -s "$TMP/det0" "$TMP/det$i" || same=0 +done +if [ ! -s "$TMP/det0" ]; then + no "F: --lint produced 0 B — an empty output compares identical to itself, which proves nothing" +elif [ "$same" -eq 1 ]; then + ok "F: 5 --lint runs over the 3-file fixture byte-identical ($( wc -c < "$TMP/det0" | tr -d ' ' ) B) — the shared compiled regex is read-only across the pool" +else + no "F: --lint is not deterministic across runs" +fi + +echo +if [ "$fail" -eq 0 ]; then echo "ALL PASS"; exit 0; else echo "SOME CHECKS FAILED"; exit 1; fi diff --git a/test/regression.sh b/test/regression.sh index cb15623dc..9ad6933b7 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From b65bb1e5cac1808815577fbcdc9e0622c60a8973 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 18:58:56 -0400 Subject: [PATCH 21/73] test(cache): gate ONE root key for every cache family, and prove it red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-1 landed the byte-budget pin but stated its own gap: llvm's qchurn blob keyed on a DIFFERENT root spelling (6b73c58ba5897c7a) from the family key (4280d3ca01d82374), so a divergent-spelling family that grew large was still evictable by the very root writing it. Two new arms in test/evictioncheck.sh make that answerable by a gate rather than by an eyeball on one corpus: (k) prime every family a normal session writes (default map, --for, --edit-check, --quality-delta, --cochange) against ONE root, then read the 16-hex root field off every blob name by the SAME rule cacheBlobRootKey uses. There must be EXACTLY ONE distinct value, and qchurn must carry it. (l) the key is a property of the ROOT, not its SPELLING: re-prime through `$R/` (trailing slash) and through a symlink; no new key may appear. RED-FIRST against the pre-change binary (5723b2c0, lane C's tip) — 3 FAILs, every pre-existing arm (a)-(j) still PASS: FAIL (k) 2 distinct root keys for ONE root — a family outside the winning key is unpinnable: ripwire-2dcb80adfcee28f9-rich.bin key=2dcb80adfcee28f9 ripwire-2dcb80adfcee28f9-lean.bin key=2dcb80adfcee28f9 ripwire-qchurn-127417b581e58c53--56b15799358c22a5.bin key=127417b581e58c53 ripwire-qsnap-127417b581e58c53-…-b4749535f4b10bf8.bin key=127417b581e58c53 ripwire-qheadsnap-127417b581e58c53-…-b4749535f4b10bf8.bin key=127417b581e58c53 FAIL (k) qchurn key '127417b581e58c53' != lean key '2dcb80adfcee28f9' — the pin cannot reach it FAIL (l) 2 distinct root keys after re-priming through '$R/' and a symlink So it is not an llvm accident and not a qchurn accident: the split reproduces on a four-file fixture, on every root, and it is lean/rich vs EVERY sha-keyed family — the gate says which half of the dir the pin was covering. The arm needs git (the qchurn/qheadsnap/qsnap families do not exist without it) and says so rather than comparing one family against itself and passing blind. No new gate file, so test/regression.sh and the gate count are unchanged. Co-Authored-By: Claude Fable 5.1 --- test/evictioncheck.sh | 121 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 117 insertions(+), 4 deletions(-) diff --git a/test/evictioncheck.sh b/test/evictioncheck.sh index 878351ade..e93dfff17 100755 --- a/test/evictioncheck.sh +++ b/test/evictioncheck.sh @@ -37,6 +37,10 @@ # root's blob instead, and says so once on stderr. # (i) P1-1: when the pinned set ALONE exceeds the budget it is kept anyway, said once on stderr. # (j) P1-1: a sweep that evicts nothing writes ZERO bytes to stderr (the disclosure is conditional). +# (k) ONE ROOT KEY FOR EVERY FAMILY: prime lean/rich/qheadsnap/qsnap/qchurn against one root; every blob +# name must carry the SAME 16-hex root field, qchurn included (P1-1's stated gap). +# (l) that key is a property of the ROOT, not its SPELLING: a trailing slash and a symlinked path add no +# new key. # # Sparse filler (truncate -s) keeps the ">2 GB" file logically oversized (what fs::file_size measures) # without touching real disk, so the gate stays fast — which is also why arms (h)-(j) can exercise the REAL @@ -265,10 +269,11 @@ kill "$HOLDER" 2>/dev/null; wait "$HOLDER" 2>/dev/null # line is a plain stderr emit, never DEGRADED_PATH_ALERT: NDEBUG compiles the alert out and the # whole point is that a Release binary discloses this too. # (j) the disclosure is CONDITIONAL: a run whose sweep evicts nothing writes ZERO bytes to stderr. -# The PIN KEY is the 16-hex fnv1a64(realpath(root)) field that every family's filename already carries — -# defaultCachePath's `ripwire--{lean,rich}.bin` and shaKeyedCachePath's -# `ripwire----.bin` alike (headSnapRepoHex hashes the same material as -# defaultCachePath), so no plumbing is needed: the sweep reads it off `keepPath` itself. +# The PIN KEY is the 16-hex root field that every family's filename carries — defaultCachePath's +# `ripwire--{lean,rich}.bin`, mcpCachePath's `ripwire-mcp-.cache` and shaKeyedCachePath's +# `ripwire----.bin` alike, so no plumbing is needed: the sweep reads it +# off `keepPath` itself. That all three really do SPELL it the same way is arms (k)/(l) below — when these +# arms were written they did not, and (h) was pinning only half the dir. # # The AGE pass is deliberately NOT pinned — a blob nobody has touched in 30 days is stale by the hygiene # policy's own definition, and its eviction costs one cold parse rather than a self-sustaining ping-pong. @@ -380,5 +385,113 @@ rc6=$? [ ! -s "$TMP5/run.err" ] && ok "(j) a sweep that evicts nothing writes ZERO bytes to stderr" \ || { no "(j) stderr is not empty on a no-eviction run — the disclosure is unconditional"; cat "$TMP5/run.err"; } +# ---- (k)(l) ONE ROOT KEY FOR EVERY CACHE FAMILY ----------------------------------------------------- +# +# THE DEFECT (2026-09-10, the follow-up to P1-1's stated gap). The pin in (h) is only as wide as the set of +# blobs that SPELL the root the same way, and two spellings shipped. Both builders hash realpath(root) with +# FNV-1a, but with DIFFERENT offset bases: +# main.cpp::defaultCachePath seeded 1469598103934665603 (17 digits — a truncated basis) +# quality.h::headSnapRepoHex seeded 14695981039346656037 (the real FNV-1a 64 basis) +# so ONE root produced TWO key families, always, on every corpus. Measured on llvm-project: lean/rich carried +# 4280d3ca01d82374 while qchurn carried 6b73c58ba5897c7a. Reproduced on a four-file fixture in one command: +# `ripwire-844a155665d606eb-{lean,rich}.bin` beside `ripwire-qchurn-526f2ad625b9f069--….bin`. Consequence: +# the byte-budget pin covers lean+rich and leaves qheadsnap/qsnap/qbody/qhist/qms/qchurn/stier unpinned — a +# git-metadata family that grew large under the divergent spelling is still evicted out from under the very +# root that is writing. +# +# (k) prime EVERY family a normal session writes (default map, --for, --edit-check, --quality-delta, +# --cochange) against ONE root, then read the 16-hex root field off every blob name by the SAME rule +# cacheBlobRootKey uses (the first '-'-delimited field that is exactly 16 hex digits). There must be +# EXACTLY ONE distinct value, and a qchurn blob must be among the blobs carrying it. Red-first: the +# pre-change binary yields two. +# (l) the key is a property of the ROOT, not of its SPELLING: the same tree addressed with a trailing +# slash and through a symlink must not add a single new key (realpath-normalized before hashing). +# +# git is required for the qchurn/qheadsnap/qsnap families to exist at all; without it those verbs degrade to +# the uncached walk and write no blob, so the arm would compare one family against itself and pass blind. +if ! command -v git >/dev/null 2>&1; then + no "(k)(l) git is required to prime the qchurn/qheadsnap/qsnap families — cannot run" +else + +# the root field, by cacheBlobRootKey's own rule: FIRST '-'-delimited field of the basename that is +# exactly 16 hex digits. Prints nothing for a blob that carries no such field. +blobrootkey(){ + basename "$1" | sed -E 's/\.(bin|cache)$//' | awk -F- '{ for( i = 1; i <= NF; ++i ) if( $i ~ /^[0-9a-f]{16}$/ ) { print $i; exit } }' +} +# every distinct root key present under a cache dir, sorted+uniqued +allrootkeys(){ + local f + while IFS= read -r f; do + blobrootkey "$f" + done < <( find "$1" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*' 2>/dev/null ) | sort -u +} +# prime every family a normal session writes, against root spelling $2, cache base $1 +primeallfamilies(){ + local cb="$1" rt="$2" + env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" >/dev/null 2>&1 + env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" --for="how does pinme work" >/dev/null 2>&1 + env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" --edit-check=pinme >/dev/null 2>&1 + env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" --quality-delta >/dev/null 2>&1 + env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" --cochange=f.cpp >/dev/null 2>&1 +} + +TMP6="$( mktemp -d )"; trap 'rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4" "$TMP5" "$TMP6"' EXIT +CB6="$TMP6/cachebase"; CD6="$CB6/ripwire"; mkdir -p "$CD6" +R6="$TMP6/repo"; mkdir -p "$R6" +cat > "$R6/f.cpp" <<'EOF_K' +int pinme( void ) +{ + return 1; +} +int other( void ) +{ + return pinme() + 1; +} +EOF_K +( cd "$R6" && git init -q . && git add -A && git -c user.email=g@g -c user.name=g commit -qm init ) >/dev/null 2>&1 + +primeallfamilies "$CB6" "$R6" + +blobs6="$( find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*' 2>/dev/null | wc -l | tr -d ' ' )" +[ "$blobs6" -ge 4 ] && ok "(k) primed $blobs6 cache blobs across the families one session writes" \ + || no "(k) only $blobs6 blob(s) written — the families under test were never primed" + +find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-qchurn-*' 2>/dev/null | grep -q . \ + && ok "(k) the qchurn family is present (the family P1-1 named as unpinned)" \ + || no "(k) no qchurn blob was written — --cochange did not memoize, arm proves nothing" + +keys6="$( allrootkeys "$CD6" )" +nkeys6="$( printf '%s\n' "$keys6" | grep -c . )" +if [ "$nkeys6" -eq 1 ]; then + ok "(k) ONE root key for every family: $keys6" +else + no "(k) $nkeys6 distinct root keys for ONE root — a family outside the winning key is unpinnable:" + find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*' 2>/dev/null | while IFS= read -r f; do + printf ' %s key=%s\n' "$( basename "$f" )" "$( blobrootkey "$f" )" + done +fi + +# the qchurn blob must carry the SAME key as the main parse cache, by name — the specific gap P1-1 stated. +lean6="$( find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*-lean.bin' 2>/dev/null | head -1 )" +churn6="$( find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-qchurn-*' 2>/dev/null | head -1 )" +if [ -n "$lean6" ] && [ -n "$churn6" ]; then + kl6="$( blobrootkey "$lean6" )"; kc6="$( blobrootkey "$churn6" )" + [ -n "$kl6" ] && [ "$kl6" = "$kc6" ] && ok "(k) qchurn carries the main parse cache's root key ($kl6)" \ + || no "(k) qchurn key '$kc6' != lean key '$kl6' — the pin cannot reach it" +else + no "(k) missing a lean or a qchurn blob to compare (lean='$lean6' churn='$churn6')" +fi + +# ---- (l) trailing slash and a symlinked spelling of the SAME tree add no new key -------------------- +ln -s "$R6" "$TMP6/link" +primeallfamilies "$CB6" "$R6/" +primeallfamilies "$CB6" "$TMP6/link" +keysl="$( allrootkeys "$CD6" )" +nkeysl="$( printf '%s\n' "$keysl" | grep -c . )" +[ "$nkeysl" -eq 1 ] && ok "(l) trailing-slash and symlinked spellings of one root keep ONE key ($keysl)" \ + || { no "(l) $nkeysl distinct root keys after re-priming through '\$R/' and a symlink — the key follows the SPELLING, not the tree:"; printf ' %s\n' $keysl; } + +fi + [ "$fail" -eq 0 ] && echo "evictioncheck: ALL PASS" || { echo "evictioncheck: SOME CHECKS FAILED"; exit 1; } From 6afaa457a414ffa856441d84e14ee185446280b0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:02:13 -0400 Subject: [PATCH 22/73] fix(capsweep): a $A in a tree-sitter pattern is not an environment variable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The executability census earned its keep on its first real run. Re-running `screen` on the fixed harness printed: EXECUTABILITY (baseline arm): 150/195 answered | 2 unparseable | 1 unexpanded variable | … unexpanded: $A, B, C . --pattern='rankGraphTeleport($A, $B, $C)' $A/$B/$C are tree-sitter METAVARIABLES. The refusal rule added one commit earlier — "an unresolved variable RAISES rather than passing through as a literal" — turned a legitimate corpus row into a non-answer, which is the same class of defect as the one it was written to remove: a row that measures nothing while the run reports success. It is only visible at all because the census now prints WHY each row failed to answer; under the old harness it would have been another silent 0. shlex.split has already discarded the quoting by the time expandvars_from sees the word, so single-quoted (no shell expansion) and double-quoted cannot be told apart there. Naming the namespace is what makes the rule decidable: a $NAME matching RIPWIRE_ is this harness's to bind and an unbound one is refused (the F17 shape — $RIPWIRE_CAPSWEEP_TMP written into the frozen corpus as a relative path); anything else passes through exactly as written, which is what the shell would have done. GATE. capsweepcheck's synthetic corpus grows a seventh row, `--stub-metavar='fn($A, $B, $C)'`, which must ANSWER, and the refusal row is respelled $RIPWIRE_CAPSWEEP_NO_SUCH_VAR so it still exercises the namespace it is about. Red-proven by reverting the namespace test: FAIL (J) a tree-sitter metavariable was refused as an unexpanded environment variable FAIL (G) 3/7 answered | 2 unexpanded variable (the row stopped answering) FAIL (I) the split was not reported over the answering rows (1 of 3, not 1 of 4) The interrupted sweep was killed rather than published around: its screen.tsv had one of 195 rows recorded as unexpanded, and a record with a known hole in it is worth less than the twenty minutes it costs to take again. --- bench/capsweep/capsweep.py | 34 ++++++++++++++++++++++++++-------- test/capsweepcheck.sh | 26 ++++++++++++++++++++------ 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index 8d4a8a596..b3a834a0e 100644 --- a/bench/capsweep/capsweep.py +++ b/bench/capsweep/capsweep.py @@ -70,7 +70,13 @@ HERE = pathlib.Path(__file__).resolve().parent REPO = HERE.parent.parent DECL = re.compile(r'^(\s*inline\s+)constexpr(\s+)([\w:<>, ]*?)(\s+)(k[A-Z][A-Za-z0-9_]*)(\s*=\s*)([0-9][0-9_.eE+-]*)(\s*;)(.*)$') -KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width') +# Same NAME vocabulary as docs/limits_build.py, and widened on the same day for the same reason: a cap +# whose name carries no keyword is invisible to the WHOLE instrument — not patched, not bumped, never in +# docs/TUNING.md. kHandoffSymbolsPerFile truncated output and disclosed syms_capped="1" while appearing +# in neither this file's census nor the register's, which is how it was raised on 2026-09-10 without +# ever having been listed anywhere. `Per\w*File`, not `PerFile`: a filter that turns on an exact compound +# spelling is the defect, not a smaller instance of it. +KEY = re.compile(r'Max|Cap|Limit|Top|Budget|Ceil|Threshold|Rows|Len|Depth|Width|Shown|Hits|Per\w*File') SHIM = """ // ── capsweep shim (SCRATCH BUILD ONLY — never in src/) ─────────────────────────────────────────── // Guarded, not `#pragma once`-protected: the shim is injected into SEVERAL headers, so a translation @@ -163,8 +169,17 @@ def answered( sizes, states, line ): VAR = re.compile(r'\$(\w+)|\$\{(\w+)\}') +# The variables this harness BINDS. Everything outside this namespace is not an environment reference at +# all and passes through untouched — `. --pattern=\'rankGraphTeleport($A, $B, $C)\'` is a tree-sitter pattern +# whose $A/$B/$C are METAVARIABLES, and the first cut of the rule below refused that row as "unexpanded", +# turning a legitimate measurement into a non-answer. shlex.split has already discarded the quoting by the +# time we see the word, so single-quoted (no expansion) and double-quoted cannot be told apart here; naming +# the namespace is what makes the rule decidable. Caught by the executability census on the re-run — the +# census earns its keep the first time it runs. +HARNESS_VAR = re.compile(r'^RIPWIRE_') + def expandvars_from(word, env): - """Expand $VARS from the environment the CHILD will get — not from os.environ. + """Expand THIS HARNESS's $VARS from the environment the CHILD will get — not from os.environ. os.path.expandvars reads os.environ, and the harness binds RIPWIRE_CAPSWEEP_TMP in a dict it hands subprocess.run. With the variable unset in the operator's shell — the normal case, and the one the @@ -173,16 +188,19 @@ def expandvars_from(word, env): the frozen corpus (a 10.4 MB cache blob). `--batch=$RIPWIRE_CAPSWEEP_TMP` read that blob back and "responded" to 103 of 108 caps: its input was the accumulated output of the run measuring it. - An unresolved variable RAISES rather than passing through as a literal. os.path.expandvars leaves it - alone, which is the shell's rule and exactly the behaviour that turned a variable into a path. + An unresolved variable in the HARNESS's own namespace RAISES rather than passing through as a literal: + os.path.expandvars leaves it alone, which is the shell's rule and exactly the behaviour that turned + $RIPWIRE_CAPSWEEP_TMP into a relative path inside the frozen corpus. A name outside that namespace is + not this harness's business and is left exactly as written. """ missing = [] def one(m): name = m.group(1) or m.group(2) - if name not in env: - missing.append(name) - return m.group(0) - return env[name] + if name in env: + return env[name] + if HARNESS_VAR.match(name): + missing.append(name) # ours to bind, and we did not — that is the F17 shape + return m.group(0) # not ours: a metavariable, a regex, someone else's literal out = VAR.sub(one, word) if missing: raise KeyError(', '.join(sorted(set(missing)))) diff --git a/test/capsweepcheck.sh b/test/capsweepcheck.sh index f542887be..94e5e466d 100755 --- a/test/capsweepcheck.sh +++ b/test/capsweepcheck.sh @@ -227,7 +227,8 @@ cat > "$TMP/rc/corpus.txt" <<'CORPEOF' . --stub-refuse . --stub-unbalanced="oops . --stub-tmp=$RIPWIRE_CAPSWEEP_TMP -. --stub-undefined=$CAPSWEEP_NO_SUCH_VAR +. --stub-undefined=$RIPWIRE_CAPSWEEP_NO_SUCH_VAR +. --stub-ok --stub-metavar='fn($A, $B, $C)' CORPEOF rc_out="$TMP/rc.out" # env -u, not `VAR=`: an empty binding is not the operator's normal case, and it used to resolve to the @@ -245,7 +246,7 @@ else # (G) the unbalanced-quote row is UNPARSEABLE, and the rows AFTER it still ran. The second half is # the F1b control: `except ValueError as e` shadows run_corpus's env dict `e`, and Python deletes an # except-name at block end, so the obvious repair kills the NEXT row with UnboundLocalError. - if grep -q 'unparseable' "$rc_out" && grep -Eq '^EXECUTABILITY.*: 3/6 answered' "$rc_out"; then + if grep -q 'unparseable' "$rc_out" && grep -Eq '^EXECUTABILITY.*: 4/7 answered' "$rc_out"; then ok "(G) an unbalanced quote is recorded UNPARSEABLE and the rows after it still run" else no "(G) unparseable row not classified, or the rows after it did not run: $( grep -m1 EXECUTABILITY "$rc_out" )" @@ -258,8 +259,8 @@ else no "(H) the refusing row was not recorded as a distinct state: $( grep -- '--stub-refuse' "$TMP/rc-screen.tsv" )" fi # (I) the denominator is the ANSWERING rows: 1 of 3, never 1 of 6. - if grep -q 'cap-sensitive: 1 of 3 answering rows' "$rc_out"; then - ok "(I) the split is reported over the 3 answering rows, not over all 6" + if grep -q 'cap-sensitive: 1 of 4 answering rows' "$rc_out"; then + ok "(I) the split is reported over the 4 answering rows, not over all 7" else no "(I) the split was not reported over the answering rows: $( grep -m1 'cap-sensitive' "$rc_out" )" fi @@ -270,8 +271,8 @@ else fi # (J) $VARS expand from the environment the harness hands the child, and an UNDEFINED one is refused # rather than passed through as a literal path (that literal is what wrote 10.4 MB into the corpus). - if grep -q 'unexpanded: \$CAPSWEEP_NO_SUCH_VAR' "$rc_out"; then - ok "(J) a row naming an undefined variable is REFUSED, not run with the literal \$NAME" + if grep -q 'unexpanded: \$RIPWIRE_CAPSWEEP_NO_SUCH_VAR' "$rc_out"; then + ok "(J) an undefined variable in the HARNESS's namespace is REFUSED, not run with the literal \$NAME" else no "(J) an undefined variable was passed through as a literal — the F17 shape" fi @@ -282,6 +283,19 @@ else fi fi + # (J) A $NAME OUTSIDE THE HARNESS'S NAMESPACE IS NOT AN ENVIRONMENT REFERENCE. The first cut of the + # rule above refused `--pattern='rankGraphTeleport($A, $B, $C)'` — a tree-sitter pattern whose $A/$B/$C + # are METAVARIABLES — as "unexpanded", turning a legitimate corpus row into a non-answer. shlex.split + # has already dropped the quoting by then, so single-quoted and double-quoted cannot be told apart: + # naming the namespace is what makes the rule decidable. + if grep -q 'unexpanded: \$A' "$rc_out"; then + no "(J) a tree-sitter metavariable was refused as an unexpanded environment variable" + elif awk -F'\t' '/stub-metavar/ { exit !($4 == "ok") }' "$TMP/rc-screen.tsv"; then + ok "(J) a \$A metavariable outside the RIPWIRE_ namespace is passed through and the row ANSWERS" + else + no "(J) the metavariable row did not answer: $( grep -- 'stub-metavar' "$TMP/rc-screen.tsv" )" + fi + # (J) control — a destination that resolves INSIDE the corpus is refused. `--cache=`, `--export=` and # `--html=` all take one, and run_corpus runs with cwd=corpus, so this is the surface that put a 10.4 MB # cache blob in the frozen tree. From 68a735d8391784537d4a3e6a2c49f5baccc3a4b9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:09:12 -0400 Subject: [PATCH 23/73] =?UTF-8?q?perf(emit):=20the=20escapers=20copy=20run?= =?UTF-8?q?s=20instead=20of=20bytes=20=E2=80=94=20measured=20first,=20and?= =?UTF-8?q?=20the=20SIMD=20scan=20refused?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S5 of the string-technique map, MEASURED BEFORE TOUCHED, as the audit's own condition demands ("adopt only if >=3% busy"). `sample` at 1 ms over warm `--top-k=100000` runs, inclusive share of the call graph: corpus surface runs busy escaper share ripwire's tree XML 48 1516 escapeXml 4.62% ripwire's tree JSON 48 1578 jsonesc::escapeInto 6.34% go XML 8 4908 escapeXml 1.43% go JSON 8 4845 escapeInto 1.88% django XML 8 2633 escapeXml 1.67% django JSON 8 2628 escapeInto 2.93% Above the bar on one of the two named corpora, below it on the other — so the rewrite proceeds and the win is stated for what it is: corpus-shaped. Escaping scales with emitted map bytes while the rest of a warm run scales with graph size, so a doc-comment-dense tree pays 3-4x the share go does. WHAT CHANGED. Both escapers keep their switch byte-for-byte and gain a run-copy skip in the for loop's INIT and INCREMENT: appendCleanRun finds the next byte the switch has an opinion about, appends everything before it in one memcpy, and returns that index. Placing it in the increment (not the body) is why jsonesc::escapeInto keeps every one of its `continue` arms and why neither function gains a branch — escapeXml's complexity is unchanged at 14, escapeInto's moves 31 -> 33 (minor). The byte set is derived from the switch and documented as derivable WITH it. AFTER, same instrument, same box: escapeXml 4.62% -> 1.97%, escapeInto 6.34% -> 2.61%. Whole-verb CPU (user+sys, 12 interleaved batched arms, median | min, load 29-39 the whole time): ripwire tree XML 4.44 -> 4.43 s (-0.2% med, -5.6% min) ripwire tree JSON 4.18 -> 3.99 s (-4.5% med, -3.9% min) django XML 2.96 -> 2.86 s (-3.5% med) django JSON 4.24 -> 4.11 s (-3.1% med, -6.7% min) go XML 6.45 -> 6.28 s (-2.7% med) go JSON 5.78 -> 5.71 s (-1.0% med) Honest reading: the escaper itself is ~2.3x cheaper; the verb is 0-4.5% cheaper, which on a loaded box is at the edge of what an A/B can resolve. The halved sampler share is the claim; the whole-run number is reported, not leaned on. THE SIMD SCAN IS REFUSED, WITH NUMBERS. Routing this through strkern::findByteset made it WORSE, not better: escapeXml went to 22.46% of a warm map (from 4.62%) and the whole map got 6-18% slower. Cause: findByteset ends every call in findByteset_scalar, which is the harness ORACLE — it re-derives a four-word bitmap from the 32-byte set on every call, 256 iterations, deliberately in a different representation so a packing bug cannot hide behind it. Right for a gate, fatal for a hot path whose inputs are 6-40 bytes. So src/infra/strkern_find.h (a sibling, per the lane's brief; strkern.h is not touched) carries the shipped scan: one O(1) bit test per byte over the SAME Byteset256, no preamble. No NEON/AVX2 block loop was written either, and that is also a measurement: benchmarked beside the scalar scan across three length bands (6..40, 60..200, 200..900) and two special-byte densities, a NEON scan is a wash below ~200 bytes — the lengths ripwire actually emits — and worth at most ~1.3x on long sparse text, i.e. a slice of a slice of a 4.62% site. Duplicating another lane's block loop for that would buy a clone. The run-copy SHAPE is where the win is (1.5x-3x in every band); the scan under it is not. Headroom recorded in the header instead of taken. appendCdataSafe is NOT rewritten: measured 0.00% of busy on `--pack-task` over both go (102,550 samples) and ripwire's own tree, and 0.02% on llvm `--grep`. It is covered by the gate anyway, so a later lane that finds it hot inherits the proof. BYTE-IDENTICAL, PROVED: 24 of 24 outputs cmp-equal against the pre-change binary — 3 corpora (ripwire's tree, go, django) x 8 surfaces (--top-k XML/JSON, --for XML/JSON, --pack-task, --grep, --lint, --hotspots), stdout and stderr and exit code. Plus test/emitescapecheck.sh's 222,682 adversarial inputs against the frozen per-byte references (landed in the previous commit, before this code existed), determinism (two runs cmp-equal, XML and JSON), and `xmllint --noout` clean. --quality-delta gating=0. --- src/infra/jsonesc.h | 45 ++++++++++++++++++++- src/infra/strkern_find.h | 86 ++++++++++++++++++++++++++++++++++++++++ src/serialize.h | 24 ++++++++++- 3 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 src/infra/strkern_find.h diff --git a/src/infra/jsonesc.h b/src/infra/jsonesc.h index 0b20a0f81..95b0993d8 100644 --- a/src/infra/jsonesc.h +++ b/src/infra/jsonesc.h @@ -36,6 +36,11 @@ // this header is a pure internal refactor: verified byte-identical against the pre-unification // implementations. +#include "strkern_find.h" // S5: findBytesetRun — the run-copy skip that replaces escapeInto's per-byte switch. + // Still zero includes ABOVE src/infra (strkern.h itself pulls only // + // / plus the ISA intrinsic header), so the no-cycle property this + // header was factored out for is intact. + #include #include #include @@ -124,13 +129,49 @@ inline int utf8SeqLen( const char* s, std::size_t i, std::size_t n ) noexcept // original from. The honest fix is a TELL on the XML side (the lossy side discloses that it substituted), // which lives in serialize.h and belongs to the lane that owns it; recorded here so the next reader of THIS // file knows the asymmetry is a decision rather than an oversight, and does not "fix" it by degrading JSON. +// S5 — THE BYTE SET IS THE CONTRACT, and here it depends on the two dialect flags. A byte NOT in the +// set reaches `out += char( c )` unchanged, so the run loop may memcpy it in bulk; a byte that IS in the +// set still goes through the SAME switch below, one at a time. Derived from that switch and re-derived +// with it: `"` and `\` always (JSON's mandatory pair), the whole C0 range always (short forms plus +// \u00XX), `<` `>` `&` only when escapeAngleAmp hardens them, and every byte >= 0x80 only when +// validateUtf8 makes utf8SeqLen decide. With validateUtf8 off, a byte >= 0x80 is a raw passthrough — +// exactly a clean-run byte — which is why the set must not contain it in that posture. +inline constexpr strkern::Byteset256 jsonEscapeByteset( bool escapeAngleAmp, bool validateUtf8 ) noexcept +{ + strkern::Byteset256 set; + set.addRange( 0x00, 0x1F ); + set.add( '"' ); + set.add( '\\' ); + if( escapeAngleAmp ) + { + set.add( '<' ); set.add( '>' ); set.add( '&' ); + } + if( validateUtf8 ) + { + set.addRange( 0x80, 0xFF ); + } + return set; +} + +// The four postures, resolved at compile time and indexed by the two flags — no per-call set building. +inline constexpr strkern::Byteset256 kJsonEscapeBytesets[ 4 ] = { + jsonEscapeByteset( false, false ), + jsonEscapeByteset( true, false ), + jsonEscapeByteset( false, true ), + jsonEscapeByteset( true, true ), +}; + inline void escapeInto( std::string_view s, std::string& out, bool escapeAngleAmp, bool validateUtf8, bool replacementAsTextEscape ) { const char* d = s.data(); const std::size_t n = s.size(); - std::size_t i = 0; - while( i < n ) + const strkern::Byteset256& set = kJsonEscapeBytesets[ ( escapeAngleAmp ? 1u : 0u ) | ( validateUtf8 ? 2u : 0u ) ]; + // Init and increment skip to the next byte this posture has an opinion about, appending the clean + // run in one go. The increment also runs on `continue`, which is what lets every arm below keep its + // own `continue` unchanged. + for( std::size_t i = strkern::appendCleanRun( d, 0, n, set, out ); i < n; + i = strkern::appendCleanRun( d, i, n, set, out ) ) { const unsigned char c = static_cast( d[i] ); diff --git a/src/infra/strkern_find.h b/src/infra/strkern_find.h new file mode 100644 index 000000000..57b230e36 --- /dev/null +++ b/src/infra/strkern_find.h @@ -0,0 +1,86 @@ +#pragma once + +// strkern_find.h — the SHIPPED byte-set scan, sibling to strkern.h's kernels. +// +// WHY THIS IS NOT `strkern::findByteset`. strkern.h's `findByteset` ends every call — SIMD path or not — +// in `findByteset_scalar`, and that function is deliberately an ORACLE: it re-derives a four-word bitmap +// from the 32-byte set on every call (a 256-iteration loop) using a DIFFERENT representation, precisely +// so a bug in the (b >> 3, b & 7) packing cannot hide behind a reference that shares it. That is exactly +// right for the gate it was written for and exactly wrong for a hot path whose inputs are SHORT: a +// symbol name, a path, a signature. Measured on this box (see the lane's report — 20k strings, best of 5, +// escapeXml end to end), routing escapeXml through `strkern::findByteset` made it 3.5x-7x SLOWER than +// the per-byte switch it replaced on 6..40-byte inputs, and `escapeXml` went from 4.62% of a warm +// `--top-k=100000` map to 22.46% of one. The 256-iteration preamble dominates everything else. +// +// So the shipped scan is this: one pass, one O(1) bit test per byte, no per-call preamble, the SAME +// `strkern::Byteset256` representation (one definition of the set, shared with the oracle that checks it). +// +// AND NO SIMD, ON PURPOSE. A NEON/AVX2 block loop over the set was measured beside this one across +// three length bands (6..40, 60..200, 200..900) and two special-byte densities. It is a wash below +// ~200 bytes — the strings ripwire actually emits — and worth at most ~1.3x on long sparse text, which +// is a slice of a slice: the whole escaper is 4.62% (XML) / 6.34% (JSON) of a warm map on ripwire's own +// tree and under 2% on the go corpus. Duplicating strkern.h's block loop here to chase that would buy a +// clone of another lane's kernel for a fraction of a fraction. The run-copy shape is where the win is +// (1.5x-3x, every band, every density); the scan under it is not. +// +// The headroom is real and recorded rather than taken: if `findByteset_scalar` ever stops being the tail +// of `findByteset` — i.e. if strkern.h grows a shipped tail beside its oracle — this file collapses into +// a call to it and the SIMD path comes along for free. That is the fold-back, and it belongs to the lane +// that owns strkern.h. + +#include "strkern.h" + +#include +#include +#include + +namespace rw::strkern +{ + +// Index of the first byte of [p, p+n) that is IN `set`, or n when none is. Pure, allocation-free, +// locale-independent; n == 0 returns 0. +inline std::size_t findBytesetRun( const char* p, std::size_t n, const Byteset256& set ) noexcept +{ + std::size_t k = 0; + while( k < n && !set.contains( static_cast( p[k] ) ) ) + { + ++k; + } + return k; +} + +// THE RUN-COPY STEP, so that neither escaper grows a shape around it. Appends the bytes from d[i] up to +// (not including) the next byte that is in `set` — the run the caller's per-byte switch has no opinion +// about — and returns the index of that byte, or n when the rest is clean. A zero-length run appends +// nothing, so the caller needs no emptiness test. +// +// Written to sit in a `for`'s INIT and INCREMENT slots: +// for( std::size_t i = appendCleanRun( d, 0, n, set, out ); i < n; i = appendCleanRun( d, i, n, set, out ) ) +// which is why it takes the index rather than a pointer and returns the next one. That placement is not +// cosmetic: the increment expression also runs on `continue`, so an escaper whose switch arms end in +// `continue` (jsonesc::escapeInto) keeps every one of them, and the loop keeps the SINGLE branch it had +// before the rewrite — the run-copy costs the escapers no measured complexity, which is the difference +// between a gated --quality-delta row and none. +// +// ONE template, not two overloads — a second body differing only in how it spells "append k bytes" is a +// 48-token clone of the first, and --quality-delta says so out loud. The spelling is picked by +// `if constexpr`: std::string (jsonesc's sink) has the (pointer, count) append and it is measurably the +// faster of the two, std::vector (serialize's sink) has only the iterator-pair insert. Both take a +// contiguous-range memcpy underneath; the difference is the length arithmetic libc++ has to redo when it +// is handed iterators instead of a count, and on strings this short that arithmetic is not free. +template< typename Sink > +inline std::size_t appendCleanRun( const char* d, std::size_t i, std::size_t n, const Byteset256& set, Sink& out ) +{ + const std::size_t clean = findBytesetRun( d + i, n - i, set ); + if constexpr( requires { out.append( d + i, clean ); } ) + { + out.append( d + i, clean ); + } + else + { + out.insert( out.end(), d + i, d + i + clean ); + } + return i + clean; +} + +} // namespace rw::strkern diff --git a/src/serialize.h b/src/serialize.h index fdc7c9cf8..09da4cc00 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -15,6 +15,7 @@ #include "redact.h" // deterministic secret redaction of emitted body content (opt-out --no-redact) #include "infra/sortutil.h" // numeric-key radix helpers for rank/file score order #include "infra/jsonesc.h" // F9: jsonesc::utf8SeqLen — the canonical UTF-8-sequence-length core (was duplicated here) +#include "infra/strkern_find.h" // S5: findBytesetRun — the run-copy skip that replaces escapeXml's per-byte switch #include "notes.h" // L3: field-notes NoteIndex — the retrieval-time surfacing lookup (INERT when null) #include "pageview.h" // §P8: pageWindow / pageDisclosure — the shared --limit/--offset contract (packDeps) #include "sarif.h" // R-E (2026-08-17): rootRelativeUri/rootPrefixOf — the same root= single-root-only @@ -124,6 +125,23 @@ using jsonesc::utf8SeqLen; // reference (xmlControlCharRef — see M2 above: G4 + attribute-value normalization); an invalid UTF-8 sequence // (A4-F20) is scrubbed to '?' so the emitted name/path/doc-comment/sig text is always well-formed XML AND // valid UTF-8 regardless of source bytes. +// S5 — THE BYTE SET IS THE CONTRACT. Everything below that is NOT in this set is copied through +// unchanged by the switch's `default:` arm, so the run loop may memcpy it in bulk without looking at it; +// everything that IS in the set still goes through the SAME switch, one byte at a time, unchanged. The +// set is therefore derivable from the switch and must be re-derived with it: the five entity bytes, the +// whole C0 range (\t \n \r become character references, every other C0 is scrubbed to a space by +// xmlSafeByte), and every byte >= 0x80 (utf8SeqLen decides whether the sequence is copied or scrubbed +// to '?'). 0x7F is deliberately absent — xmlSafeByte passes DEL through, so it is a clean-run byte. +// test/emitescapecheck.sh's MUT arm exists because a set one member short is otherwise silent. +inline constexpr strkern::Byteset256 kXmlEscapeByteset = [] +{ + strkern::Byteset256 set; + set.addRange( 0x00, 0x1F ); + set.add( '&' ); set.add( '<' ); set.add( '>' ); set.add( '"' ); set.add( '\'' ); + set.addRange( 0x80, 0xFF ); + return set; +}(); + inline std::string_view escapeXml( std::string_view s, std::vector& out ) { out.clear(); @@ -132,7 +150,11 @@ inline std::string_view escapeXml( std::string_view s, std::vector& out ) const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; const char* d = s.data(); const std::size_t n = s.size(); - for( std::size_t i = 0; i < n; ) + // Init and increment skip to the next byte the switch actually has an opinion about, copying + // everything before it in one insert. On ordinary source text — names, paths, signatures, + // doc-comments — that run is the whole string: one scan and one memcpy for the whole call. + for( std::size_t i = strkern::appendCleanRun( d, 0, n, kXmlEscapeByteset, out ); i < n; + i = strkern::appendCleanRun( d, i, n, kXmlEscapeByteset, out ) ) { const char c = d[i]; switch( c ) From ecaff19feafd13c8c6a28b048d34ab80f77951ea Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:11:44 -0400 Subject: [PATCH 24/73] quality(clones): an overload set, one file, and vendored upstream are not your duplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Duplication's gating precision over 40 replayed commits was ZERO — 11 gating rows, 9 noise and 2 wrong — and the three mechanisms behind them are properties of the GROUP, not of its text, so all three are decidable without touching the clone matcher: (a) ONE OVERLOAD SET — every member shares one canonical id. `emitTo|emitTo`, `sort::stable|sort::stable`: overloads are near-identical by construction, and reporting them as a copy is reporting the language. (b) ONE FILE AND NO REUSED MEMBER — a sibling pair inside one body of code (`mergeHi|mergeLo`, `gallopLeft|gallopRight`) is an alternate implementation the author is looking at while writing it. The second half of that clause is not a hedge: copying a helper three call sites already use is a real erosion whether the copy lands next door or across the tree. Dropping on file identity ALONE silently retired test/clonededupcheck.sh's entire positive case, which is how the clause was found — the gate went red, and it was right. (c) VENDORED — every member sits under a vendored path. No such notion existed anywhere in quality.h, and one commit (08416403, the timsort landing) produced 9 duplication rows, 8 of 8 dead-code:new-symbol acks and 37 api-surface acks against an upstream body whose shape is not this repo's to fix. `.ripwire_config` grew its SECOND key, `vendored_paths = PATH[, PATH...]`, beside four built-in conventions. Honest scope, measured while writing the gate: the CRAWLER already drops third_party/, vendor/ and node_modules/, so `external/` is the only built-in the indexer reaches and everything else vendored is reached through the config key. NO TOKEN FLOOR. Raising kMinCloneTokens was measured and REFUTED: the canonical true positive (synthetic S1, a 12-line copy of a reused helper) is 59 tokens while the idiom collisions in the same replay run 22, 24, 31, 36, 56, 65, 66, 74, 78, 91, 92, 96, 114 and 127. A floor above 22 loses true positives before it clears any noise; token count is the wrong axis and the comment in the code says so. | | wt before | wt after | ref before | ref after | | rows | 266 | 208 | 259 | 117 | | duplication rows | 9 | 8 | 23 | 14 | | new-clone rows | 1 | 1 | 3 | 2 | | gating rows | 171 | 32 | 69 | 23 | | commits that gate | 12/12 | 8/12 | 20/40 | 11/40 | | gating precision TRUE | 2% | 12% | 10% | 30% | | WRONG rows (gating) | 1 | 0 | 6 | 0 | | WRONG rows (all) | 1 | 0 | 26 | 6 | (cumulative with the four dials before it; "before" is the pre-branch binary.) WRONG reaches ZERO on the gating population of both replays here. 6 of 6 and 9 of 9 TRUE-or-chronic gating rows survive this dial with nothing lost and nothing demoted. WHAT IS NOT FIXED, and it is the honest remainder: 9 gating duplication rows survive in the ref-pair population and every one is the same shape — a one-line AST-tag predicate matched against another one in a different language (`sliceIsJsPatternKind|cc_isParamList`). They share no domain identifier, which is the discriminator the ledger's own acks use in words. cloneidiom.h exists for exactly this and recognizes three closed shapes; it emitted `idiom=` on 0 of 32 clone rows across 52 replayed documents, so its shape set matches none of what this repo actually produces. That is a cloneidiom.h round, not a filter this commit can add. GATE: test/qddialscheck.sh §5, nine arms over four STRUCTURALLY distinct clone shapes — four copies of ONE body collapse into a single six-member group and made the first draft of this fixture vacuous in every arm. Three arms RED on the pre-change binary (one-file, external/, and the config key, which the old binary also reported as an unrecognized key). The cross-file copy — synthetic S1's shape, the reason these kinds exist — must survive all three drops, and one arm proves external/ is actually indexed so the vendored arm is not passing on a directory the crawler never enters. clonebandcheck, cloneidiomcheck, clonededupcheck, type3clonecheck, freshclonecheck, registermacrocheck: PASS. Co-Authored-By: Claude Fable 5.1 --- src/quality.h | 161 +++++++++++++++++++++++++++++++++++++++++-- test/qddialscheck.sh | 70 +++++++++++++++++++ 2 files changed, 225 insertions(+), 6 deletions(-) diff --git a/src/quality.h b/src/quality.h index 404041bf9..fff0c0cf4 100644 --- a/src/quality.h +++ b/src/quality.h @@ -138,7 +138,7 @@ inline std::string acksPath( const std::string& root ) { return rootQualifie // reader, no `RIPWIRE_CONFIG` constant). Smallest thing consistent with the two house sidecar // conventions already in the tree — `.ripwire_notes` (committed, degrade-don't-throw, absent=inert) and // `.ripwire_quality_acks` (root-qualified via rootQualifiedSidecar, never the process CWD): a committed, -// human-editable key=value text file at the repo root. ONE recognized key today (readRegisterMacrosConfig +// human-editable key=value text file at the repo root. TWO recognized keys today (readRegisterMacrosConfig // below); an unrecognized key is skipped rather than refused, so the file can grow new keys later without // a binary that predates them choking on it — notes.h's own forward-compat rule, restated here for a new // file rather than invented twice. @@ -337,10 +337,16 @@ inline bool isValidMacroToken( std::string_view token ) noexcept struct RegisterMacrosConfig { std::vector names; // valid register_macros=NAME tokens, sorted + deduped - std::vector unrecognizedKeys; // distinct non-"register_macros" keys seen, sorted + deduped + std::vector vendoredPaths; // Q-DIAL-5: vendored_paths=PATH[, PATH...] tokens, sorted + deduped + std::vector unrecognizedKeys; // distinct key seen that is neither of the two above, sorted + deduped }; +// NAME NOTE: this type and its reader are spelled for the FIRST key they carried, and they keep those names +// on purpose — test/qschemetripcheck.sh's manifest keys the determinism guard on the function NAME +// `readRegisterMacrosConfig`, so renaming it for tidiness would silently retire a guard. It is the +// .ripwire_config reader; it reads two keys. -// `.ripwire_config`'s ONE recognized key: `register_macros = NAME[, NAME...]`. Grammar: one directive per +// `.ripwire_config`'s TWO recognized keys: `register_macros = NAME[, NAME...]` and, since 2026-09-10, +// `vendored_paths = PATH[, PATH...]` (Q-DIAL-5 — code this repo carries but did not write). Grammar: one directive per // line, '#' full-line comments, blank lines ignored; a line with no '=' at all carries no key/value shape // this file defines anything for, so it is left alone rather than guessed at (same "never throws, never // guesses" posture as the malformed-token skip below). A line that DOES have that shape but whose key is @@ -376,8 +382,10 @@ inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) } std::string_view key = line.substr( 0, eq ); while( !key.empty() && ( key.back() == ' ' || key.back() == '\t' ) ) { key.remove_suffix( 1 ); } - constexpr std::string_view kKey = "register_macros"; - if( key != kKey ) + constexpr std::string_view kKey = "register_macros"; + constexpr std::string_view kVendorKey = "vendored_paths"; // Q-DIAL-5 + const bool isVendor = key == kVendorKey; + if( key != kKey && !isVendor ) { out.unrecognizedKeys.emplace_back( key ); // F-13: disclosed, not skipped continue; @@ -390,7 +398,17 @@ inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) std::string_view tok( rest.data() + start, ( comma == std::string_view::npos ? rest.size() : comma ) - start ); while( !tok.empty() && ( tok.back() == ' ' || tok.back() == '\t' ) ) { tok.remove_suffix( 1 ); } while( !tok.empty() && ( tok.front() == ' ' || tok.front() == '\t' ) ) { tok.remove_prefix( 1 ); } - if( isValidMacroToken( tok ) ) + if( isVendor ) + { + // A PATH, not an identifier: root-relative, no '..' segment, no leading '/' — anything else is + // a value this file's grammar defines nothing for and is dropped rather than guessed at, the + // same posture the macro-token check takes. + if( !tok.empty() && tok.front() != '/' && tok.find( ".." ) == std::string_view::npos ) + { + out.vendoredPaths.emplace_back( tok ); + } + } + else if( isValidMacroToken( tok ) ) { out.names.emplace_back( tok ); } @@ -403,6 +421,8 @@ inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) } std::sort( out.names.begin(), out.names.end() ); out.names.erase( std::unique( out.names.begin(), out.names.end() ), out.names.end() ); + std::sort( out.vendoredPaths.begin(), out.vendoredPaths.end() ); + out.vendoredPaths.erase( std::unique( out.vendoredPaths.begin(), out.vendoredPaths.end() ), out.vendoredPaths.end() ); std::sort( out.unrecognizedKeys.begin(), out.unrecognizedKeys.end() ); out.unrecognizedKeys.erase( std::unique( out.unrecognizedKeys.begin(), out.unrecognizedKeys.end() ), out.unrecognizedKeys.end() ); return out; @@ -422,6 +442,61 @@ inline std::vector registeredMacroNames( std::string_view root ) return names; } +// Q-DIAL-5 (2026-09-10) — VENDORED PATHS: code this repo CARRIES but did not WRITE. No such notion existed +// anywhere in this file, and the clone kinds paid for it: one commit (08416403, the timsort landing) produced +// 9 duplication rows, 8 of 8 dead-code:new-symbol acks and 37 api-surface acks against an upstream body whose +// shape is not this repo's to fix. The ledger says so in its own words, 11 times. +// +// Built-in conventions plus whatever `.ripwire_config`'s vendored_paths= adds. The built-ins are the four +// directory names the ecosystem agrees on; a vendored file that lives somewhere else (this repo's own +// src/infra/timsort.hpp) is exactly what the config key is for, because no convention can guess it. +// HONEST SCOPE, measured while writing the gate for this: the CRAWLER already drops third_party/, vendor/ +// and node_modules/, so those three names are here for completeness rather than effect — `external/` is the +// only built-in the indexer actually reaches, and everything else vendored is reached through the config key. +inline constexpr std::array kBuiltinVendoredPrefixes = { "third_party/", "vendor/", "node_modules/", "external/" }; + +inline std::vector vendoredPathPrefixes( std::string_view root ) +{ + std::vector out; + for( std::string_view p : kBuiltinVendoredPrefixes ) + { + out.emplace_back( p ); + } + for( std::string& extra : readRegisterMacrosConfig( root ).vendoredPaths ) + { + out.push_back( std::move( extra ) ); + } + std::sort( out.begin(), out.end() ); + out.erase( std::unique( out.begin(), out.end() ), out.end() ); + return out; +} + +// `rel` is ROOT-RELATIVE (the relForHash spelling every sidecar key uses). A prefix ending in '/' names a +// DIRECTORY and matches everything under it; one that does not is a whole path and must match exactly, so +// `vendored_paths = src/infra/timsort.hpp` cannot silently swallow src/infra/timsort_extra.hpp. +inline bool isVendoredPath( std::string_view rel, const std::vector& prefixes ) noexcept +{ + for( const std::string& p : prefixes ) + { + if( p.empty() ) + { + continue; + } + if( p.back() == '/' ) + { + if( rel.size() >= p.size() && rel.compare( 0, p.size(), p ) == 0 ) + { + return true; + } + } + else if( rel == p ) + { + return true; + } + } + return false; +} + // A registered macro's own call syntax, read starting at the CALLEE's own signature start byte (`region` // begins at sigStartByte — either forEachSymbolBody's per-symbol slice below, or a caller's own substr): // a leading identifier exactly matching one of `names`, then optional whitespace, then '('. Linear scan @@ -6005,6 +6080,71 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap const std::vector exactIdioms = classifyCloneGroupIdioms( ing, exactClones ); const std::vector type3Idioms = classifyCloneGroupIdioms( ing, type3Clones ); + // ── Q-DIAL-5 (2026-09-10) — three shapes that are not THIS CHANGE'S duplication ───────────────────── + // Duplication's gating precision over 40 replayed commits was 0%: 11 gating rows, 9 noise and 2 wrong. + // Three mechanisms produced them, and each is a property of the GROUP rather than of its text, so each is + // decidable here without touching the clone matcher: + // (a) ONE OVERLOAD SET — every member shares one canonical id. Overloads of a function are near- + // identical by construction (emitTo|emitTo, sort::stable|sort::stable); reporting them as a copy is + // reporting the language. + // (b) ONE FILE AND NO REUSED MEMBER — every member lives in the same file and none of them is a helper + // the tree already leans on (fan-in >= kReusedHelperMinFanin). A sibling pair inside one body of + // code is an alternate implementation the author is looking at while writing it (mergeHi|mergeLo, + // gallopLeft|gallopRight), not the reuse decline these kinds exist to catch. The fan-in half is not + // a hedge: copying a helper that three call sites already use is a real erosion whether the copy + // lands next door or across the tree, and dropping it on file identity alone silently retired + // test/clonededupcheck.sh's whole positive case — which is how this clause was found. + // (c) VENDORED — every member sits under a vendored path (see isVendoredPath). Upstream's shape is not + // this repo's to fix, and one commit produced 9 such rows. + // NOT a token floor: raising kMinCloneTokens was measured and REFUTED. The canonical true positive + // (synthetic S1, a 12-line copy of a reused helper) is 59 tokens, while the idiom collisions in the same + // replay run 22, 24, 31, 36, 56, 65, 66, 74, 78, 91, 92, 96, 114 and 127 — a floor above 22 loses true + // positives before it clears any noise. Token count is the wrong axis. + const std::vector vendoredPrefixes = vendoredPathPrefixes( root ); + const auto cloneGroupIsOutOfScope = [ & ]( const CloneGroup& cg ) + { + if( cg.members.size() < 2 ) + { + return false; + } + const auto* ro = g.inEdges.rowOffsets(); + bool oneId = true; + bool oneFile = true; + bool allVend = true; + bool reused = false; + std::string_view firstId; + std::uint32_t firstFile = 0; + bool haveFirst = false; + for( NodeId m : cg.members ) + { + if( m >= ing.symbols.size() || m >= g.canonId.size() ) + { + return false; // unclassifiable member — never claim a whole-group property + } + const std::uint32_t f = ing.symbols[m].fileId; + if( f >= ing.files.size() ) + { + return false; + } + if( !isVendoredPath( relForHash( ing.files[f], root ), vendoredPrefixes ) ) + { + allVend = false; + } + if( std::uint32_t( ro[m + 1] - ro[m] ) >= kReusedHelperMinFanin ) + { + reused = true; + } + if( !haveFirst ) + { + firstId = g.canonId[m]; firstFile = f; haveFirst = true; + continue; + } + if( g.canonId[m] != firstId ) { oneId = false; } + if( f != firstFile ) { oneFile = false; } + } + return oneId || ( oneFile && !reused ) || allVend; + }; + gtl::btree_map dupSeen; const auto reportNewClones = [ & ]( const std::vector& cgs, const std::vector& vx ) @@ -6031,6 +6171,10 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap { continue; } + if( cloneGroupIsOutOfScope( cg ) ) + { + continue; // Q-DIAL-5 — an overload set, one file, or vendored upstream (see the block above) + } if( !dupSeen.insert( { h, 1 } ).second ) { continue; // same member-set already reported this run @@ -6418,6 +6562,11 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap { continue; // no PREEXISTING reused helper in the group } + if( cloneGroupIsOutOfScope( cg ) ) + { + continue; // Q-DIAL-5 — the same three shapes, on the same groups: a helper cannot have + // eroded its own reuse by being overloaded, and an upstream body is not ours. + } if( !reuseSeen.insert( { h, 1 } ).second ) { continue; // same member-set already reported diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 60f4b783d..42a8c63e7 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -254,5 +254,75 @@ printf '%s' "$OAP" | grep -q 'api-new-surface="1"' \ && ok "api-surface: byte-identical run to run (deterministic)" || no "api-surface: non-deterministic delta" +# ── 5) duplication: an overload set, one file, and vendored upstream are not this change's copies ──────── +# 11 gating duplication rows over 40 replayed commits, 0% precision: overload pairs (emitTo|emitTo, +# sort::stable|sort::stable), sibling implementations inside one body of code (mergeHi|mergeLo, +# gallopLeft|gallopRight), and one commit's 9 rows against vendored upstream. The cross-file copy of a real +# helper — synthetic S1, the shape these kinds exist for — must survive all three drops. +DP="$WORK/dup"; mkdir -p "$DP/src" "$DP/src/infra" "$DP/external" +( cd "$DP" && git init -q && git config user.email t@t && git config user.name t && git config commit.gpgsign false ) +# FOUR DISTINCT SHAPES, one per case. The clone matcher normalizes identifiers, so four copies of one body +# collapse into a SINGLE six-member group and no per-case assertion can separate them — the first draft of +# this fixture did exactly that and every arm was vacuous. Each shape below differs STRUCTURALLY (different +# statements, different control flow), so each case forms its own two-member group. +clonebody(){ python3 - "$1" "$2" <<'PY' +import sys +name, shape = sys.argv[1], sys.argv[2] +bodies = { + "1": " int acc = 0;\n for( int i = 0; i < n; ++i ) {\n if( i % 3 == 0 ) { acc += i * 2; }\n else if( i % 5 == 0 ) { acc -= i; }\n else { acc += 1; }\n }\n if( acc < 0 ) { acc = 0; }\n return acc;\n", + "2": " int total = n;\n while( total > 1 ) {\n total = total / 2;\n total = total + 7;\n if( total > 900 ) { break; }\n }\n for( int k = 0; k < 4; ++k ) { total ^= k; }\n return total;\n", + "3": " int r = 1;\n switch( n % 4 ) {\n case 0: r = n + 11; break;\n case 1: r = n - 11; break;\n case 2: r = n * 3; break;\n default: r = n / 2; break;\n }\n do { r += 5; } while( r < 0 );\n return r;\n", + "4": " int q = 0;\n for( int a = 0; a < n; ++a ) {\n for( int b = 0; b < a; ++b ) { q += a * b; }\n }\n q = q > 1000 ? 1000 : q;\n q = q - ( n % 17 );\n return q;\n", +} +print( "int %s( int n ){\n%s}" % (name, bodies[shape]) ) +PY +} +clonebody alpha 1 > "$DP/src/a.cpp" +clonebody sameA 2 > "$DP/src/same.cpp" +clonebody vendA 3 > "$DP/external/v1.cpp" +clonebody cfgA 4 > "$DP/src/infra/w1.hpp" +printf 'int drive(){ return alpha(1) + sameA(1) + vendA(1) + cfgA(1); }\n' > "$DP/src/drive.cpp" +( cd "$DP" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) +# the working edit: four copies, one per shape. +clonebody beta 1 > "$DP/src/b.cpp" # CROSS-FILE — must still be reported +clonebody sameB 2 >> "$DP/src/same.cpp" # same file +clonebody vendB 3 > "$DP/external/v2.cpp" # both members vendored by the built-in convention +clonebody cfgB 4 > "$DP/src/infra/w2.hpp" # vendored only if .ripwire_config says so +ODP="$( cd "$DP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +dup(){ rows "$ODP" | grep 'kind="duplication"' | grep "$1"; } +dup 'alpha' >/dev/null && ok "duplication: the CROSS-FILE copy is still reported (synthetic S1's shape)" \ + || { no "duplication: the cross-file copy was dropped — the dial cut a true positive"; rows "$ODP" | grep duplication; } +dup 'sameA' >/dev/null \ + && { no "duplication: a group confined to ONE FILE is still reported"; rows "$ODP" | grep duplication; } \ + || ok "duplication: a group confined to one file produces no row" +# NON-VACUITY, checked rather than assumed: three of the four built-in prefixes (third_party/, vendor/, +# node_modules/) are already dropped by the CRAWLER, so a fixture placed there would pass this arm on any +# binary ever built — the first draft of it did. external/ is the one the crawler indexes, so it is the one +# that can prove the rule. +( cd "$DP" && "$BIN" . --top-k=100000 --no-cache 2>/dev/null ) | grep -q 'vendA' \ + && ok "duplication: the external/ pair IS indexed (the vendored arm is not vacuous)" \ + || no "duplication: external/ is not indexed — the vendored arm proves nothing" +dup 'vendA' >/dev/null \ + && { no "duplication: a group inside external/ is still reported"; rows "$ODP" | grep duplication; } \ + || ok "duplication: a group under a built-in vendored prefix produces no row" +dup 'cfgA' >/dev/null && ok "duplication: src/infra/ IS reported with no .ripwire_config (the control)" \ + || { no "duplication: the config control is vacuous — src/infra/ was already silent"; rows "$ODP" | grep duplication; } +# now name it vendored, and only that row goes away. +printf 'vendored_paths = src/infra/\n' > "$DP/.ripwire_config" +ODP2="$( cd "$DP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +rows "$ODP2" | grep 'kind="duplication"' | grep -q 'cfgA' \ + && { no "duplication: vendored_paths= in .ripwire_config did not exempt src/infra/"; rows "$ODP2" | grep duplication; } \ + || ok "duplication: vendored_paths=src/infra/ in .ripwire_config exempts the group" +rows "$ODP2" | grep 'kind="duplication"' | grep -q 'alpha' \ + && ok "duplication: the cross-file copy survives the config key too" \ + || no "duplication: vendored_paths= swallowed an unrelated group" +printf '%s' "$ODP2" | grep -q 'config-warnings=' \ + && { no "duplication: vendored_paths= was reported as an unrecognized .ripwire_config key"; } \ + || ok "duplication: vendored_paths= is a RECOGNIZED key (no config-warnings on the root)" +rm -f "$DP/.ripwire_config" +[ "$ODP" = "$( cd "$DP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "duplication: byte-identical run to run (deterministic)" || no "duplication: non-deterministic delta" + + [ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" exit "$fail" From 99b41edb0bd3d94dc0ece1015ffcd31e06cd67c3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:18:43 -0400 Subject: [PATCH 25/73] =?UTF-8?q?perf(slice):=20the=20def-use=20walk=20ind?= =?UTF-8?q?exed=20a=20file-wide=20child=20list=20=E2=80=94=20and=20the=20g?= =?UTF-8?q?ate=20that=20catches=20all=20ten?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GATE FIRST. test/childwalkscalecheck.sh (gate #588) covers the ten surviving unbounded `ts_node_child( n, i )` walks that audit P1-0's follow-up table named. Six isolation arms, each the SAME fixture width with the walk entered and not entered, so a red arm names ONE walk rather than "the verb got slower". Proven RED against the pre-change binary (lane W's tip e246ca25), all six: (B1) sliceWalk --slice vs the plain map 64.0 x 1.28s / 0.02s (B2) collectSpanTiers --grep vs the plain map 61.5 x 1.23s / 0.01s (B3) measureFileHealth one error token vs none 60.5 x 1.21s / 0.01s (B4) ffiVisitNode extern "C" vs the same flood 11.2 x 1.34s / 0.12s (B5) ln_collectLocalDecls --naming-locals vs --lint 11.3 x 1.35s / 0.12s (B6) findMatches/matchChildren --pattern vs the plain map 125.3 x 3.76s / 0.03s WHAT MAKES IT QUADRATIC, MEASURED. `ts_node_child` is O(C) only when the child list is FLAT. A grammar REPEAT is a balanced tree of invisible nodes that `ts_node__child` skips in O(1), so a root of 128 000 DECLARATIONS is dead linear (8k/64k/128k = 0.04 / 0.33 / 0.63 s on the pre binary). What is not balanced is what the parser splices into the child array: EXTRAS — comments above all. A root of 16 000 COMMENTS is 117 x its control. Every fixture here is therefore a comment flood; a declaration flood of the same width ships a green gate over a live defect. Recorded on src/infra/tschildren.h and in the gate header. THIS COMMIT converts src/slice.h's two walks (sliceWalk, sliceWalkPreproc) and adds `forEachChild` to src/infra/tschildren.h — one spelling of the cursor idiom, `fn -> bool` so a filtering or searching walk gets its `break`; `appendChildren` is rewritten on top of it. Both slice walks recurse from inside the loop, so each frame owns its cursor; the header says why the cursor is an explicit parameter rather than an implicit one. sliceWalkPreproc has NO scaling arm and the gate header carries both measurements that say why: its natural control (the same flood, `#if` removed) routes through sliceWalk's own quadratic loop and reads 0.98 x on BOTH binaries, and a 1k->16k ratio cannot go green because --slice's rung-3 flow walk (SliceRdWalker, ~11 `ts_node_named_child` loops) is a LARGER quadratic on the same path that this lane does not own. --slice over a 16 000-comment definition went 2.38 s -> 1.21 s here; the residual 1.21 s is `ts_node_named_child` in a sample of the fixed binary. Handed to the next lane whole. Byte-identical, --no-cache, new binary vs e246ca25: ripwire / go / canyonraid48 x {map --top-k=100000, --for, --grep, --pack-task, --match, --lint --naming-locals, --dead-code, --pattern} = 24/24, plus --slice on four ripwire symbols and one canyon symbol = 5/5, plus the gate's own 30 generated and 21 committed fixture x verb pairs. Gates: childwalkscale, slice, sliceflow, sliceflowsens, slicediff, pattern, matchgrammar, lint, naminglocals, naminglens, ffi, greptier, grepfast, parsehealth, pyimportprecise, route, padscale, preprocdeadscale, manifest, gatecount, xmlwellformed, shellgateindex, binoverride, infraport, includeangle — ALL PASS. Co-Authored-By: Claude Fable 5.1 --- README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/infra/tschildren.h | 45 ++++- src/slice.h | 25 ++- test/childwalkscalecheck.sh | 341 +++++++++++++++++++++++++++++++++ test/regression.sh | 2 +- 7 files changed, 403 insertions(+), 26 deletions(-) create mode 100755 test/childwalkscalecheck.sh diff --git a/README.md b/README.md index d157ea9f3..b72eb8f4f 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-587 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +588 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 **587 gate scripts** and is the authoritative list; +`test/regression.sh` names **588 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index 1b1058118..6144576de 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` | 587 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 588 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) @@ -5625,7 +5625,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 **587 gate scripts**, all of which exist on disk. +naming **588 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`. @@ -6637,7 +6637,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 587. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 588. 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/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index fc5a0220f..606d106f2 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["587 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 + ["588 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "587", "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, "588", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["588 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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/infra/tschildren.h b/src/infra/tschildren.h index 3da5d5cf0..c3ea50037 100644 --- a/src/infra/tschildren.h +++ b/src/infra/tschildren.h @@ -23,6 +23,19 @@ // gets broken, so the rule and the helper now live where every walk can reach them. Gates: // test/padscalecheck.sh (the comment flood) and test/preprocdeadscalecheck.sh (the include-guard flood, // which the `#if`-text gate in preprocdead.h hides from the first). +// +// WHICH WIDE NODES ACTUALLY COST O(C²) — MEASURED, 2026-09-10 (lane W2, test/childwalkscalecheck.sh). A +// FLAT child list does; a grammar REPEAT does not. tree-sitter stores a repetition as a balanced tree of +// invisible `_repeat` nodes, and `ts_node__child` skips a whole invisible subtree in O(1) by reading its +// stored `visible_child_count` (`ts_node__relevant_child_count`, node.c) — so indexing the 128 000th +// declaration of a file scope is ~O(log C), and a declaration flood measured dead linear on the +// pre-change binary (8k/64k/128k children = 0.04 / 0.33 / 0.63 s). What is NOT balanced is anything the +// parser splices into the child array itself: EXTRAS (comments, above all) and preprocessor-conditional +// bodies. A root of 16 000 COMMENTS measured 117× its own control on the same binary. The practical rule +// is therefore not "wide node" but "wide node whose width can come from EXTRAS", which — since a comment +// can appear between any two children of anything — is every walk whose node comes from the FILE. It is +// also why a scaling gate must flood with comments: a declaration flood of identical width goes green +// over a live defect. #include @@ -40,24 +53,42 @@ struct ChildCursor // RAII — several walkers return mid-loop, so deletion mu ~ChildCursor() { ts_tree_cursor_delete( &cur ); } }; -// APPEND n's children, left to right, to whatever `out` already holds. This is the form a DFS-STACK walk -// needs: there the collected list IS the work list, so clearing it would throw the frontier away. Routing -// such a walk through collectChildren instead costs it a scratch vector plus a copy of every node; the two -// forms measured indistinguishably on this box (both inside a ±3% noise band that a same-binary control -// reproduced with the opposite sign), so this exists for the shape, not for a measured win. -inline void appendChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates +// VISIT n's children, left to right, without materialising them — the one spelling of the cursor idiom +// every other function here is written on. `fn( TSNode ) -> bool` returns false to STOP, which is the +// `break` a filtering or searching walk needs and the `continue` case falls out of returning true. +// +// It takes the node AND the cursor because the two lifetimes differ: a walk that only filters can hand +// the same cursor to every node it visits, while a walk that RECURSES from inside `fn` cannot — the +// recursive call resets the cursor out from under the loop — and must own one per frame (`ChildCursor +// cursor( n ); forEachChild( n, cursor.cur, … )`). Making the cursor implicit would have hidden exactly +// that distinction, which is the bug this whole header exists to prevent. +template< class Fn > +inline void forEachChild( TSNode n, TSTreeCursor& cur, const Fn& fn ) // A4-F25: NOT noexcept — `fn` may allocate { ts_tree_cursor_reset( &cur, n ); if( ts_tree_cursor_goto_first_child( &cur ) ) { do { - out.push_back( ts_tree_cursor_current_node( &cur ) ); + if( !fn( ts_tree_cursor_current_node( &cur ) ) ) + { + return; + } } while( ts_tree_cursor_goto_next_sibling( &cur ) ); } } +// APPEND n's children, left to right, to whatever `out` already holds. This is the form a DFS-STACK walk +// needs: there the collected list IS the work list, so clearing it would throw the frontier away. Routing +// such a walk through collectChildren instead costs it a scratch vector plus a copy of every node; the two +// forms measured indistinguishably on this box (both inside a ±3% noise band that a same-binary control +// reproduced with the opposite sign), so this exists for the shape, not for a measured win. +inline void appendChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates +{ + forEachChild( n, cur, [ &out ]( TSNode child ) { out.push_back( child ); return true; } ); +} + // REPLACE `out` with n's children — the form a walker uses when it wants one node's child list as a // standalone array to scan or index. Delegates, so there is exactly one spelling of the cursor idiom. inline void collectChildren( TSNode n, TSTreeCursor& cur, std::vector& out ) // A4-F25: NOT noexcept — `out` allocates diff --git a/src/slice.h b/src/slice.h index eebe1812e..f522fbb6c 100644 --- a/src/slice.h +++ b/src/slice.h @@ -48,6 +48,7 @@ // */src/parser.c), not assumed from upstream docs. #include "preprocdead.h" // #62: the ONE literal `#if 0`/`#if 1` rule, shared with the ingest call-ref pass +#include "infra/tschildren.h" // ChildCursor/forEachChild — both walks below descend from the FILE root #include "infra/sortutil.h" #include "model.h" #include "ingest.h" // sliceGrammarForFile — path → grammar, ingest's one table @@ -1084,17 +1085,20 @@ inline void sliceWalkPreproc( TSNode node, const SliceWalkCtx& ctx, SliceScan& s const TSNode condition = sliceField( node, "condition" ); const TSNode macroName = sliceField( node, "name" ); const TSNode alternative = sliceField( node, "alternative" ); - const std::uint32_t ppChildCount = ts_node_child_count( node ); - for( std::uint32_t childIndex = 0; childIndex < ppChildCount; ++childIndex ) + // O(children), not O(children²): a `#if` block's child list is the whole guarded region, INCLUDING + // every comment in it as a direct child (extras are spliced into the array — src/infra/tschildren.h). + // The cursor is this frame's own because `walk` recurses back into here. + ChildCursor cursor( node ); + forEachChild( node, cursor.cur, [ & ]( TSNode child ) { - const TSNode child = ts_node_child( node, childIndex ); if( ( !ts_node_is_null( condition ) && ts_node_eq( child, condition ) ) || ( !ts_node_is_null( macroName ) && ts_node_eq( child, macroName ) ) ) { - continue; // macro names and #if expressions are never variable occurrences + return true; // macro names and #if expressions are never variable occurrences } const bool isAlt = !ts_node_is_null( alternative ) && ts_node_eq( child, alternative ); walk( child, ctx, scan, isAlt ? altState : bodyState ); - } + return true; + } ); } // one occurrence node: classify, anchor, drop-or-flag by preprocessor state, bind if it introduces @@ -1152,11 +1156,12 @@ inline void sliceWalk( TSNode node, const SliceWalkCtx& ctx, SliceScan& scan, Sl return; // an identifier is a leaf — nothing beneath it } - const std::uint32_t childCount = ts_node_child_count( node ); - for( std::uint32_t i = 0; i < childCount; ++i ) - { - sliceWalk( ts_node_child( node, i ), ctx, scan, pp ); - } + // O(children), not O(children²). This walk starts at the FILE root, so the very first node it + // expands has one child per top-level construct AND one per comment between them — the width a + // 16 000-comment file hands it measured 60× its own control before this became a cursor + // (test/childwalkscalecheck.sh, arm B1). The cursor is this frame's own: the loop body recurses. + ChildCursor cursor( node ); + forEachChild( node, cursor.cur, [ & ]( TSNode child ) { sliceWalk( child, ctx, scan, pp ); return true; } ); } diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh new file mode 100755 index 000000000..6f740202c --- /dev/null +++ b/test/childwalkscalecheck.sh @@ -0,0 +1,341 @@ +#!/usr/bin/env bash +# childwalkscalecheck.sh — the SCALING gate for the ten remaining unbounded indexed child walks, and the +# answers each of them must still produce. +# +# bash test/childwalkscalecheck.sh # build/ripwire +# bash test/childwalkscalecheck.sh .ripwire_pre # the RED run (pre-change binary, indexed walks) +# RIPWIRE_BIN=asan/ripwire bash test/childwalkscalecheck.sh +# RIPWIRE_REF_BIN=/path/to/pre-change/ripwire bash test/childwalkscalecheck.sh # arm (C) +# +# WHY A THIRD SCALING GATE. test/padscalecheck.sh covers the comment flood through the INGEST walks; +# test/preprocdeadscalecheck.sh covers `collectPreprocDeadRanges` (the include-guard flood the `#if` text +# gate hides from the first). Neither reaches the ten OTHER walks that indexed their children with +# `ts_node_child( n, i )` — five of them live behind a VERB (`--slice`, `--grep`, `--pattern`, +# `--lint --naming-locals`) that no ingest-shaped gate runs, and two more only enter on a file the parser +# had to recover in (`measureFileHealth`) or on an `extern "C"` block (`ffiVisitNode`). +# +# WHAT MAKES THE WALK QUADRATIC — AND WHAT DOES NOT. `ts_node_child( n, i )` restarts tree-sitter's child +# iterator at the first child every call (see the note on src/infra/tschildren.h), so indexing C children +# costs O(C^2) — but ONLY when the child list is FLAT. A grammar REPEAT (16 000 declarations at file +# scope, 16 000 elements in one brace initializer) is stored as a balanced tree of invisible `_repeat` +# nodes, and `ts_node__child` skips a whole invisible subtree in O(1) via `ts_node__relevant_child_count` +# (third_party/deps/tree_sitter/lib/src/node.c). Measured on the pre-change binary, 2026-09-10: a root of +# 128 000 DECLARATIONS is linear (8k/64k/128k = 0.04 / 0.33 / 0.63 s), while a root of 16 000 COMMENTS is +# 117x its own control. Comments are tree-sitter EXTRAS: the parser splices them into the child array +# itself, where no repeat node balances them. Every fixture below is therefore a COMMENT flood — a +# declaration flood of the same width proves nothing and would have shipped a green gate over a live +# defect. +# +# THE FIXTURE SHAPE IS "WIDE NODE, CHEAP CHILDREN". Each fixture makes ONE node's child list N wide and +# every child trivial to process, so what the arm measures is the indexing, not the per-child work. The +# first attempt at these fixtures made the children expensive (16 000 real statements inside the sliced +# definition) and buried the walk under sliceWalkOccurrence — the ratio came out linear with the defect +# still in place. +# +# ARMS +# (A) ANSWERS — every converted verb still answers correctly ON THE FLOODED FIXTURE, so it is the +# converted wide walk that produced the answer: the slice's vars, the grep hit, the pattern match, +# the extern "C" symbol row, `parse_degraded="1"` for the recovered file, and the eight +# `naming-underscore` LOCAL rows that only --naming-locals can reach. Plus determinism. +# (B1..B6) SCALING — user CPU, every arm an ISOLATION pair: the SAME fixture width with the walk +# ENTERED and NOT entered (--slice vs the plain map, --grep vs the plain map, --pattern vs the plain +# map, --lint --naming-locals vs --lint, an error token present vs absent, `extern "C"` present vs +# absent). An isolation pair names ONE walk instead of saying "the verb got slower", and it cannot +# be defeated by a fixed start-up cost the way a 1k-vs-16k ratio can (ffiVisitNode's own 1k arm is +# 0.09 s of C++ ingest, which flattens its ratio to 13x while the walk is 13x its control). +# (C) BYTE-IDENTICAL vs RIPWIRE_REF_BIN, every fixture x every reaching verb — the conversion must not +# move one byte. SKIPPED and disclosed when RIPWIRE_REF_BIN is unset. +# (D) MUTATION — the ratio verdict and the row readers are shown able to fail. +# +# User CPU, never wall, so a loaded box cannot flake the ratios; every ratio floors its divisor so a ~0 s +# small arm cannot manufacture a large one. +# +# NOT CONVERTED, AND WHY (the rest of the audit P1-0 follow-up table, whose class 1 this gate closes): +# * src/slice.h sliceWalkPreproc IS converted, but has NO scaling arm here and cannot get one YET. Two +# measurements say why. (i) Its natural isolation control — the identical comment flood inside the +# same definition with the `#if` removed — routes through sliceWalk's own child loop, which was +# quadratic too, so the pair reads 0.98x on the pre-change binary AND 0.98x on the fixed one: it can +# never go red. (ii) A 1k->16k ratio cannot go GREEN, because --slice's rung-3 flow walk +# (SliceRdWalker, ~11 `ts_node_named_child` loops in this same file) is a LARGER quadratic on the same +# path and this lane does not own it: --slice over a 16 000-comment definition went 2.38 s -> 1.21 s +# here (this lane's half), and the residual 1.21 s is `ts_node_named_child` in a `sample` of the fixed +# binary — dominant even on a fixture whose slice resolves ZERO vars. `ts_node_named_child` is the +# SAME `ts_node__child` body with include_anonymous=false and the same restart, and a comment is a +# NAMED extra, so the whole 55-site named-child class has this defect; it is the next lane's, whole, +# rather than half-converted here. sliceWalkPreproc's conversion is gated by arms (A2) and (C). +# * src/pattern.h smallestContaining / snapshotNode ARE converted, but have NO arm here and cannot get +# one: both index the children of the PATTERN's parse tree, and pattern.h:78 caps a pattern at +# kMaxPatternBytes = 4096 — every path in, --pattern and --lint-rules alike, goes through that one +# check (pattern.h:683). 4096 bytes is ~2 000 children, i.e. ~2e6 iterator steps, ~1 ms. The cap is +# why the site was never hot; the conversion is for uniformity and is covered by arm (C). +# * src/ingest_binds.h:1343 (bindsVisitNode) keeps the indexed form: its body needs the INDEX for +# `ts_node_field_name_for_child( n, i )`, which is itself index-based, so collecting the children +# would leave the loop quadratic in the field lookup. The cursor's own O(1) +# `ts_tree_cursor_current_field_name` is the real fix and is a SEMANTIC change (alias/extra handling) +# that needs its own gate — not folded into a no-output-change lane. +# * src/ingest_names.h:61 (firstChildOfType) keeps the indexed form: both callers pass a +# `using_declaration` / `qualified_identifier`, whose width comes from the grammar, and a per-call +# cursor allocation would cost more than the scan it replaces. Class 3 in practice, not class 2. +# * The ~37 class-3 sites (base clauses, parameter/argument lists, attribute lists, fixed-index probes) +# keep the indexed form on purpose — see the note on src/infra/tschildren.h. +# +# Exit 0 = ALL PASS, non-zero = SOME FAILED. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" # allow a repo-relative RIPWIRE_BIN +REF="${RIPWIRE_REF_BIN:-}" +[ -n "$REF" ] && [ "${REF#/}" = "$REF" ] && REF="$ROOT/$REF" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +skip(){ printf ' SKIP %s\n' "$*"; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "childwalkscalecheck: python3 required"; exit 2; } +echo "childwalkscalecheck: BIN=$BIN" +[ -n "$REF" ] && echo "childwalkscalecheck: REF=$REF" + +# ── fixtures ───────────────────────────────────────────────────────────────────────────────────────── +# Generated, never committed: a committed 1 MB comment flood would join every OTHER gate's view of test/ +# (trap: a gate fixture that is also part of the live tree the tool indexes). +python3 - "$TMP" <<'PY' +import os, sys +base = sys.argv[ 1 ] +C = "// pad " + "x" * 40 + +def w( rel, lines ): + p = os.path.join( base, rel ) + os.makedirs( os.path.dirname( p ), exist_ok = True ) + open( p, 'w' ).write( "\n".join( lines ) + "\n" ) + +for n in ( 1000, 16000 ): + # sliceWalk — N comments as children of the ROOT, the sliced definition tiny and early + w( "slicew/n%d/big.c" % n, + [ "int helper( int x );", "int target( int x )", "{", " int acc = x;", + " return helper( acc );", "}" ] + [ C ] * n ) + # sliceWalkPreproc — N comments as children of ONE `#if` block INSIDE the sliced definition + w( "slicepp/n%d/big.c" % n, + [ "int helper( int x );", "int target( int x )", "{", " int acc = x;", "#if 1" ] + + [ C ] * n + [ "#endif", " return helper( acc );", "}" ] ) + # collectSpanTiers — N comments as children of the ROOT, reached by --grep's span-tier pass + w( "span/n%d/big.c" % n, [ C ] * n + [ "// needle_marker", "int s0;" ] ) + # measureFileHealth — the same flood, plus ONE token the parser must recover from (walk ENTERED) + w( "health/n%d/big.c" % n, [ C ] * n + [ "@ @ @", "int e0;" ] ) + # …and its control: byte-for-byte the same flood with no error token (walk RETURNS at the has_error check) + w( "health_off/n%d/big.c" % n, [ C ] * n + [ "int e0;" ] ) + # ffiVisitNode — N comments as children of an `extern "C"` block's declaration list (walk ENTERED) + w( "ffi/n%d/big.cpp" % n, + [ 'extern "C" {' ] + [ C ] * n + [ 'int f0( int a );', '}', 'int useit( void ) { return f0( 1 ); }' ] ) + # …and its control: the same flood and the same declarations, no linkage_specification + w( "ffi_off/n%d/big.cpp" % n, + [ C ] * n + [ 'int f0( int a );', 'int useit( void ) { return f0( 1 ); }' ] ) + # ln_collectLocalDecls — N comments in a function body wide enough to clear namingLocalsGate + # (loc bar AND locals >= 8); the eight locals carry a shape the naming rules report, so arm (A) can + # prove the re-parse walk ran at all. + w( "locals/n%d/big.c" % n, + [ "int bigfun( int x )", "{", " if( x > 0 )", " {" ] + + [ " int loc__%d = x + %d;" % ( i, i ) for i in range( 8 ) ] + + [ " x = loc__0;", " }" ] + [ C ] * n + [ " return x;", "}" ] ) + # findMatches (root flood) + matchChildren (the candidate compound_statement's own flood) + w( "pat/n%d/big.c" % n, [ "void f( void )", "{", " int a;" ] + [ C ] * n + [ "}" ] + [ C ] * n ) +PY + +# user-CPU seconds (user+sys) of one cold run of "$@" against corpus $1 +usercpu(){ # $1 = corpus dir, rest = binary + flags + local dir="$1"; shift + { /usr/bin/time -p "$@" "$dir" --no-cache >/dev/null; } 2>"$TMP/t" || { echo FAIL; return; } + awk '/^user/ { u = $2 } /^sys/ { s = $2 } END { printf "%.2f", u + s }' "$TMP/t" +} + +# The one ratio verdict, so no arm hand-rolls a second arithmetic for the same job. Prints +# "fast" | "linear" | "quad ". $1 small/control, $2 big, $3 ratio ceiling, $4 absolute short-circuit. +verdict(){ awk -v s="$1" -v b="$2" -v cap="$3" -v floor="$4" 'BEGIN { + if( b + 0 < floor + 0 ) { print "fast"; exit } # absolute cost already fine — scaling is moot + if( s + 0 < 0.02 ) { s = 0.02 } # floor the divisor: a ~0 arm cannot invent a ratio + if( b / s < cap + 0 ) { printf "linear %.1f", b / s } else { printf "quad %.1f", b / s } +}'; } + +# One scaling arm. $1 = label, $2 = control CPU, $3 = walk CPU, $4 ceiling, $5 floor, $6 = what the pair is +arm(){ + local label="$1" c="$2" w="$3" cap="$4" fl="$5" what="$6" + if [ "$c" = FAIL ] || [ "$w" = FAIL ] || [ -z "$c" ] || [ -z "$w" ]; then + no "$label a timed run failed outright"; return + fi + local v; v="$( verdict "$c" "$w" "$cap" "$fl" )" + case "$v" in + fast) ok "$label ${w}s CPU < ${fl}s — the O(C^2) walk is absent ($what, control ${c}s)";; + linear\ *) ok "$label ${v#linear } x its control (walk=${w}s control=${c}s) — under the ${cap}x ceiling";; + *) no "$label ${v#quad } x its control — $what is quadratic in child count (walk=${w}s control=${c}s)";; + esac +} + +count_rows(){ python3 -c ' +import re,sys +sys.stdout.write( str( len( re.findall( sys.argv[2], open( sys.argv[1] ).read() ) ) ) )' "$1" "$2"; } + +# ── (A) the answers, on the FLOODED fixtures ───────────────────────────────────────────────────────── +echo +echo "=== (A) every converted walk still answers, on a 1000-comment-wide node ===" + +"$BIN" "$TMP/slicew/n1000" --no-cache --slice=target >"$TMP/a_slice.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_slice.xml" '"$TMP/a_slicepp.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_slicepp.xml" '"$TMP/a_grep.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_grep.xml" 'tier="comment"' )" = 1 ] && [ "$( count_rows "$TMP/a_grep.xml" '"$TMP/a_health.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_health.xml" '"$TMP/a_ffi.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_ffi.xml" '' )" = 1 ]; then + ok "(A5) ffiVisitNode: the extern \"C\" declaration is still resolved as a call target" +else + no "(A5) ffiVisitNode: the extern \"C\" call edge is gone on the flooded fixture" +fi +"$BIN" "$TMP/locals/n1000" --no-cache --lint --naming-locals >"$TMP/a_loc_on.xml" 2>/dev/null +"$BIN" "$TMP/locals/n1000" --no-cache --lint >"$TMP/a_loc_off.xml" 2>/dev/null +A_ON="$( count_rows "$TMP/a_loc_on.xml" '"$TMP/a_pat.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_pat.xml" '' )" = 1 ]; then + ok "(A7) findMatches/matchChildren: the pattern still matches f's body past 1000 comment children" +else + no "(A7) findMatches/matchChildren: the pattern lost its match on the flooded body" +fi +"$BIN" "$TMP/slicew/n1000" --no-cache --slice=target >"$TMP/a_slice2.xml" 2>/dev/null +if [ ! -s "$TMP/a_slice.xml" ]; then + no "(A8) determinism (empty --slice answer)" +elif cmp -s "$TMP/a_slice.xml" "$TMP/a_slice2.xml"; then + ok "(A8) determinism (two cold --slice runs byte-identical, $( wc -c <"$TMP/a_slice.xml" | tr -d ' ' ) B)" +else + no "(A8) determinism (two cold --slice runs differ)" +fi + +# ── (B) scaling ────────────────────────────────────────────────────────────────────────────────────── +echo +echo "=== (B) scaling: one node's child list 16000 wide, every child trivial ===" + +b_slicew_map="$( usercpu "$TMP/slicew/n16000" "$BIN" --top-k=100000 )" +b_slicew_walk="$( usercpu "$TMP/slicew/n16000" "$BIN" --slice=target )" +arm "(B1) sliceWalk" "$b_slicew_map" "$b_slicew_walk" 8 0.30 "--slice over a 16000-comment root vs the plain map of the same file" + +b_span_map="$( usercpu "$TMP/span/n16000" "$BIN" --top-k=100000 )" +b_span_walk="$( usercpu "$TMP/span/n16000" "$BIN" --grep=needle_marker )" +arm "(B2) collectSpanTiers" "$b_span_map" "$b_span_walk" 8 0.30 "--grep's span-tier pass over a 16000-comment root vs the plain map" + +b_health_off="$( usercpu "$TMP/health_off/n16000" "$BIN" --top-k=100000 )" +b_health_on="$( usercpu "$TMP/health/n16000" "$BIN" --top-k=100000 )" +arm "(B3) measureFileHealth" "$b_health_off" "$b_health_on" 8 0.30 "one recovered token over a 16000-comment root vs the identical flood with none" + +b_ffi_off="$( usercpu "$TMP/ffi_off/n16000" "$BIN" --top-k=100000 )" +b_ffi_on="$( usercpu "$TMP/ffi/n16000" "$BIN" --top-k=100000 )" +arm "(B4) ffiVisitNode" "$b_ffi_off" "$b_ffi_on" 8 0.30 "an extern \"C\" block 16000 children wide vs the identical flood outside one" + +b_loc_off="$( usercpu "$TMP/locals/n16000" "$BIN" --lint )" +b_loc_on="$( usercpu "$TMP/locals/n16000" "$BIN" --lint --naming-locals )" +arm "(B5) ln_collectLocalDecls" "$b_loc_off" "$b_loc_on" 8 0.30 "--naming-locals' re-parse walk over a 16000-comment body vs --lint alone" + +b_pat_map="$( usercpu "$TMP/pat/n16000" "$BIN" --top-k=100000 )" +b_pat_walk="$( usercpu "$TMP/pat/n16000" "$BIN" --pattern='{ int a; ... }' )" +arm "(B6) findMatches/matchChildren" "$b_pat_map" "$b_pat_walk" 8 0.30 "--pattern over a 16000-comment root and body vs the plain map" + +# ── (C) byte-identical against a reference binary ──────────────────────────────────────────────────── +echo +echo "=== (C) byte-identical output vs RIPWIRE_REF_BIN ===" +if [ -z "$REF" ]; then + skip "(C) RIPWIRE_REF_BIN unset — no reference binary to compare against (set it to the pre-change build)" +elif [ ! -x "$REF" ]; then + no "(C) RIPWIRE_REF_BIN=$REF is not executable" +else + c_fail=0 + c_seen=0 + cmp_pair(){ # $1 = corpus, rest = flags + local dir="$1"; shift + c_seen=$(( c_seen + 1 )) + "$BIN" "$dir" --no-cache "$@" >"$TMP/c_new" 2>/dev/null + "$REF" "$dir" --no-cache "$@" >"$TMP/c_ref" 2>/dev/null + if [ ! -s "$TMP/c_ref" ]; then + no "(C) reference output of $( basename "$( dirname "$dir" )" )/$( basename "$dir" ) [$*] is empty — the comparison would be vacuous"; c_fail=1 + elif ! cmp -s "$TMP/c_new" "$TMP/c_ref"; then + no "(C) $( basename "$( dirname "$dir" )" )/$( basename "$dir" ) [$*] differs from the reference binary"; c_fail=1 + fi + } + for n in n1000 n16000; do + for d in slicew slicepp span health health_off ffi ffi_off locals pat; do + cmp_pair "$TMP/$d/$n" --top-k=100000 + done + cmp_pair "$TMP/slicew/$n" --slice=target + cmp_pair "$TMP/slicepp/$n" --slice=target + cmp_pair "$TMP/span/$n" --grep=needle_marker + cmp_pair "$TMP/health/$n" --grep=pad + cmp_pair "$TMP/locals/$n" --lint --naming-locals + cmp_pair "$TMP/pat/$n" --pattern='{ int a; ... }' + done + [ "$c_fail" = 0 ] && ok "(C1) $c_seen generated fixture x verb pairs are byte-identical to the reference" + c_fail=0 + c_seen=0 + for d in "$ROOT/test/cfix" "$ROOT/test/cppqualfix" "$ROOT/test/preproccondfix" "$ROOT/test/ffifix" "$ROOT/test/pyimportprecisefix" "$ROOT/test/sliceflowsensfix" "$ROOT/test/lintfix"; do + [ -d "$d" ] || continue + cmp_pair "$d" --top-k=100000 + cmp_pair "$d" --grep=int + cmp_pair "$d" --lint --naming-locals + done + if [ "$c_seen" = 0 ]; then + no "(C2) no committed fixture tree found — the arm would have been vacuous" + elif [ "$c_fail" = 0 ]; then + ok "(C2) $c_seen committed fixture x verb pairs are byte-identical to the reference" + fi +fi + +# ── (D) mutation: every verdict shape above is shown able to fail ──────────────────────────────────── +echo +echo "=== (D) MUTATION — the verdict and row readers are shown able to fail ===" +case "$( verdict 0.09 1.22 8 0.30 )" in + quad\ *) ok "(D) the measured pre-change ffiVisitNode pair (0.09s vs 1.22s) IS called quad";; + *) no "(D) the isolation verdict cannot see the pathology it was written against";; +esac +case "$( verdict 0.02 2.43 8 0.30 )" in + quad\ *) ok "(D) the measured pre-change findMatches pair (0.02s vs 2.43s) IS called quad";; + *) no "(D) the isolation verdict cannot see the largest pathology it was written against";; +esac +case "$( verdict 0.10 0.70 8 0.30 )" in + linear\ *) ok "(D) a 7x pair (0.10s vs 0.70s) IS called linear, not quad";; + *) no "(D) the isolation verdict calls a linear pair quadratic";; +esac +case "$( verdict 0.01 0.20 8 0.30 )" in + fast) ok "(D) a sub-0.30s walk arm short-circuits to fast";; + *) no "(D) the absolute short-circuit does not fire";; +esac +printf 'a__bq' >"$TMP/m.xml" +if [ "$( count_rows "$TMP/m.xml" '&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preprocdeadscalecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preprocdeadscalecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From f892d2b93d472d22fafc0390defeb0e5f92debdb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:18:59 -0400 Subject: [PATCH 26/73] =?UTF-8?q?perf(pattern):=20the=20structural-search?= =?UTF-8?q?=20walks=20indexed=20the=20candidate's=20children=20=E2=80=94?= =?UTF-8?q?=20--pattern=203.76s=20->=200.06s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four sites in src/pattern.h, all of audit P1-0's class 1. findMatches and matchChildren take their width from the CORPUS: findMatches is a stack walk from each file's root, matchChildren collects one candidate node's children. A comment is an extra spliced straight into the child array, so a 16 000-comment file gave both of them a 16 000-wide flat list. Gate arm (B6), test/childwalkscalecheck.sh, against the pre-change binary: --pattern over that file was 125.3 x the plain map of the SAME file (3.76 s vs 0.03 s); it is now 0.06 s vs 0.03 s. findMatches hoists one cursor and one reused `kids` vector out of the loop and pushes in REVERSE, so the stack still pops left to right — the visit order is unchanged, which is what makes the output byte-identical rather than merely equivalent. matchChildren owns its cursor per frame because matchAt recurses back into it. smallestContaining and snapshotNode index the children of the PATTERN's own parse tree, and pattern.h caps a pattern at kMaxPatternBytes = 4096 on the one path every caller takes (--pattern and --lint-rules alike, pattern.h:683) — ~2 000 children, ~2e6 iterator steps, ~1 ms. They are converted for uniformity and CANNOT get a scaling arm; the cap is why they were never hot, and the gate header says so instead of implying an arm exists. smallestContaining now uses ONE cursor for the whole descent (each level finishes before the next begins) and stops on the first containing child through forEachChild's false return. Byte-identical, --no-cache, vs lane W's tip e246ca25: --pattern and --match on ripwire / go / canyonraid48 (6/6, part of the 24/24 sweep in the previous commit), the gate's 30 generated and 21 committed fixture x verb pairs. patterncheck, matchgrammarcheck, lintcheck, lintrulescheck — ALL PASS. Co-Authored-By: Claude Fable 5.1 --- src/pattern.h | 56 +++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/pattern.h b/src/pattern.h index 40a695879..f2f626660 100644 --- a/src/pattern.h +++ b/src/pattern.h @@ -59,6 +59,7 @@ #include "model.h" #include "infra/Diagnostics.h" #include "infra/namesplit.h" // isIdentChar / isIdentStart — the ONE ASCII identifier-character pair +#include "infra/tschildren.h" // ChildCursor/forEachChild/collectChildren — the O(C) child walk (P1-0) #include @@ -419,21 +420,21 @@ inline bool isCommentKind( const char* type ) noexcept // extra/zero-width child, and so the loop is obviously bounded by tree depth. inline TSNode smallestContaining( TSNode root, std::uint32_t begin, std::uint32_t end ) { - TSNode n = root; + TSNode n = root; + ChildCursor cursor( root ); // ONE cursor for the whole descent: each level finishes before the next starts for( ;; ) { bool descended = false; - const std::uint32_t childCount = ts_node_child_count( n ); - for( std::uint32_t c = 0; c < childCount; ++c ) + forEachChild( n, cursor.cur, [ & ]( TSNode child ) { - const TSNode child = ts_node_child( n, c ); if( ts_node_start_byte( child ) <= begin && ts_node_end_byte( child ) >= end && ts_node_end_byte( child ) > ts_node_start_byte( child ) ) { n = child; descended = true; - break; + return false; // stop: this level is decided } - } + return true; + } ); if( !descended ) { return n; @@ -486,21 +487,21 @@ inline std::uint32_t snapshotNode( PatternProgram& prog, TSNode n, std::string_v // Literal: keep every child, named and anonymous alike (the `+` in `a + b` is load-bearing), minus // comments. A childless literal carries its own text and must match it exactly. std::vector kids; - const std::uint32_t childCount = ts_node_child_count( n ); - kids.reserve( childCount ); - for( std::uint32_t c = 0; c < childCount; ++c ) + kids.reserve( ts_node_child_count( n ) ); + ChildCursor cursor( n ); // this frame's own — the loop below recurses into snapshotNode + forEachChild( n, cursor.cur, [ & ]( TSNode child ) { - const TSNode child = ts_node_child( n, c ); if( isCommentKind( ts_node_type( child ) ) ) { - continue; + return true; } if( ts_node_end_byte( child ) <= ts_node_start_byte( child ) ) { - continue; // zero-width (a MISSING recovery node) — never a shape constraint + return true; // zero-width (a MISSING recovery node) — never a shape constraint } kids.push_back( child ); - } + return true; + } ); if( kids.empty() ) { PatNode& self = prog.nodes[index]; @@ -867,18 +868,21 @@ bool matchAt( const PatternProgram& prog, std::uint32_t patIndex, TSNode cand, s // function grows a second concern: matchAt decides what ONE node is, this decides how a LIST lines up. inline bool matchChildren( const PatternProgram& prog, const PatNode& pat, TSNode cand, std::string_view src, MatchEnv& env, MatchStats& stats, unsigned depth ) { + // O(children), not O(children²): `cand` is a node of the CORPUS, so its width — and, since comments + // are extras spliced into the child array, its comment count — comes from the file being searched. + // A 16 000-comment function body measured 174× the plain map of the same file before this became a + // cursor (test/childwalkscalecheck.sh, arm B7). std::vector kids; - const std::uint32_t childCount = ts_node_child_count( cand ); - kids.reserve( childCount ); - for( std::uint32_t c = 0; c < childCount; ++c ) + kids.reserve( ts_node_child_count( cand ) ); + ChildCursor cursor( cand ); // this frame's own — matchAt below recurses back into matchChildren + forEachChild( cand, cursor.cur, [ &kids ]( TSNode child ) { - const TSNode child = ts_node_child( cand, c ); - if( isCommentKind( ts_node_type( child ) ) ) + if( !isCommentKind( ts_node_type( child ) ) ) { - continue; // comments are transparent on the candidate side too + kids.push_back( child ); // comments are transparent on the candidate side too } - kids.push_back( child ); - } + return true; + } ); std::size_t ci = 0; // next unconsumed candidate child std::size_t pi = 0; // next unmatched pattern child @@ -988,6 +992,8 @@ inline void findMatches( const PatternProgram& prog, TSNode root, std::string_vi } const std::uint16_t rootKindId = prog.nodes[0].kindId; std::vector stack; + std::vector kids; // reused across nodes — a warm walk allocates nothing per node + ChildCursor cursor( root ); stack.push_back( root ); while( !stack.empty() && out.size() < budget ) { @@ -1001,10 +1007,12 @@ inline void findMatches( const PatternProgram& prog, TSNode root, std::string_vi out.emplace_back( ts_node_start_byte( n ), ts_node_end_byte( n ) ); } } - const std::uint32_t childCount = ts_node_child_count( n ); - for( std::uint32_t c = childCount; c > 0; --c ) + // Collected once, then pushed in REVERSE so the stack pops left to right — the same visit order + // the indexed loop had, at O(children) instead of O(children²) (src/infra/tschildren.h). + collectChildren( n, cursor.cur, kids ); + for( std::size_t c = kids.size(); c > 0; --c ) { - stack.push_back( ts_node_child( n, c - 1 ) ); + stack.push_back( kids[ c - 1 ] ); } } std::sort( out.begin(), out.end() ); From 0751ac54441d8b1a0f93808eb2395f1ea8dfb9d6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:19:18 -0400 Subject: [PATCH 27/73] perf(ingest): the last four file-wide child walks, plus the three class-2 sites worth converting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CLASS 1 — the remaining four of audit P1-0's follow-up table. Each is one node whose child list is as wide as the FILE lets it be, and each measured red as an isolation pair against lane W's tip e246ca25 (test/childwalkscalecheck.sh; the walk-entered arm vs the identical fixture with the walk not entered): collectSpanTiers ingest_astquery.h --grep's tier pass vs the plain map 61.5 x 1.23s/0.01s measureFileHealth ingest_crawl.h one error token vs none 60.5 x 1.21s/0.01s ffiVisitNode ingest_sidecap.h extern "C" vs the same flood outside 11.2 x 1.34s/0.12s ln_collectLocalDecls ingest_metrics.h --naming-locals vs --lint 11.3 x 1.35s/0.12s All four are now 0.01-0.14 s, i.e. indistinguishable from their own controls. The two stack walks (collectSpanTiers, measureFileHealth) hoist one cursor out of the loop — neither recurses, so one cursor serves every node — and keep their exact visit order: collectSpanTiers collects once and pushes in REVERSE (its pop order feeds a stable_sort whose input order is part of the output), while measureFileHealth filters straight into the work list. ffiVisitNode's inner DFS uses appendChildren because `inner` IS the frontier. ln_collectLocalDecls recurses, so its cursor is per frame. CLASS 2 — converted, both pure iterations whose width comes from the input and whose child list a comment can lengthen: routesVisitNode (decorators of one definition) and capturePythonImportBinds (clauses of one import statement). Neither can get a scaling arm — a 16 000-decorator definition is not a shape any corpus produces — so both are gated by the byte-identical arms only, and the gate header says that rather than implying otherwise. collectGatedLocalNames' own top-level loop goes with them (it also called ts_node_child TWICE per index). NOT converted, with the reason recorded in the gate header: bindsVisitNode needs the INDEX for `ts_node_field_name_for_child( n, i )`, which is itself index-based — collecting the children would leave the loop quadratic in the field lookup, and the cursor's O(1) field-name accessor is a semantic change that needs its own gate, not a fold into a no-output-change lane. firstChildOfType is class 3 in practice: both callers hand it a using_declaration / qualified_identifier, a grammar-bounded width, and a per-call cursor allocation would cost more than the scan it replaces. Byte-identical, --no-cache, vs e246ca25: ripwire / go / canyonraid48 x {map --top-k=100000, --for, --grep, --pack-task, --match, --lint --naming-locals, --dead-code, --pattern} = 24/24; the gate's 30 generated and 21 committed fixture x verb pairs. Gates listed in the first commit of this lane — ALL PASS. Co-Authored-By: Claude Fable 5.1 --- src/ingest_astquery.h | 21 +++++++++++++-------- src/ingest_crawl.h | 13 +++++++++---- src/ingest_metrics.h | 13 ++++++++----- src/ingest_relations.h | 22 +++++++++++++--------- src/ingest_sidecap.h | 34 ++++++++++++++++++++-------------- 5 files changed, 63 insertions(+), 40 deletions(-) diff --git a/src/ingest_astquery.h b/src/ingest_astquery.h index 00440c8a8..37d191c55 100644 --- a/src/ingest_astquery.h +++ b/src/ingest_astquery.h @@ -1200,6 +1200,8 @@ inline SpanTier spanTierOfNodeType( const char* type ) noexcept static void collectSpanTiers( TSNode root, std::uint32_t byteCount, SpanTierMap& out ) { std::vector stack; + std::vector kids; // reused across nodes — a warm walk allocates nothing per node + ChildCursor cursor( root ); stack.push_back( root ); while( !stack.empty() ) { @@ -1220,10 +1222,15 @@ static void collectSpanTiers( TSNode root, std::uint32_t byteCount, SpanTierMap& // ALL children, not just the named ones — a comment is an `extra` in most grammars and several // spell it as an anonymous node, so a named-only walk silently misses exactly the tier this // function exists to find. - const std::uint32_t childCount = ts_node_child_count( n ); - for( std::uint32_t c = childCount; c > 0; --c ) + // Collected once, then pushed in REVERSE so the stack pops left to right — the same visit order + // the indexed loop had, at O(children) instead of O(children²). The width here is the FILE's: this + // walk starts at the root, and a comment is an extra spliced straight into the child array, so a + // 16 000-comment file made --grep's tier pass 56× the plain map of the same file before this became + // a cursor (test/childwalkscalecheck.sh, arm B3; the rule is on src/infra/tschildren.h). + collectChildren( n, cursor.cur, kids ); + for( std::size_t c = kids.size(); c > 0; --c ) { - stack.push_back( ts_node_child( n, c - 1 ) ); + stack.push_back( kids[ c - 1 ] ); } } // The stack walk emits in DFS pop order, which is not byte order once a subtree is skipped; the @@ -1801,11 +1808,9 @@ std::vector collectGatedLocalNames( std::string_view defBytes, st const TSNode root = ts_tree_root_node( tree ); // the def parses as a single top-level function_definition inside a translation_unit — descend into // the translation_unit's children (bounded: one file-worth of def text, already size-capped upstream). - const std::uint32_t n = ts_node_child_count( root ); - for( std::uint32_t i = 0; i < n; ++i ) - { - ln_collectLocalDecls( ts_node_child( root, i ), ts_node_child( root, i ), 512, out, defStartLine, defBytes ); - } + ChildCursor cursor( root ); + forEachChild( root, cursor.cur, [ & ]( TSNode child ) + { ln_collectLocalDecls( child, child, 512, out, defStartLine, defBytes ); return true; } ); ts_tree_delete( tree ); ts_parser_delete( parser ); return out; diff --git a/src/ingest_crawl.h b/src/ingest_crawl.h index 68f010c87..814c9d5eb 100644 --- a/src/ingest_crawl.h +++ b/src/ingest_crawl.h @@ -492,6 +492,7 @@ FileHealth measureFileHealth( TSNode root, std::string_view bytes ) } std::vector stack; + ChildCursor cursor( root ); // reused across nodes — this walk never recurses stack.push_back( root ); while( !stack.empty() ) { @@ -510,15 +511,19 @@ FileHealth measureFileHealth( TSNode root, std::string_view bytes ) ++h.errNodes; continue; } - const std::uint32_t kids = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < kids; ++i ) + // O(children), not O(children²). The root of a RECOVERED file is exactly where the width is + // largest and least controlled — one comment flood plus one unparseable token measured 56× the + // identical flood with no error in it before this became a cursor (test/childwalkscalecheck.sh, + // arm B4; the rule is on src/infra/tschildren.h). Filtered in place: `stack` is the work list, + // and only the children that carry an error belong on it. + forEachChild( n, cursor.cur, [ &stack ]( TSNode c ) { - const TSNode c = ts_node_child( n, i ); if( ts_node_has_error( c ) || ts_node_is_missing( c ) ) { stack.push_back( c ); } - } + return true; + } ); } return h; } diff --git a/src/ingest_metrics.h b/src/ingest_metrics.h index c2206090f..10e9735c8 100644 --- a/src/ingest_metrics.h +++ b/src/ingest_metrics.h @@ -1319,11 +1319,14 @@ inline void ln_collectLocalDecls( TSNode node, TSNode funcRoot, int depth, std:: } return; // do not descend INTO a countable declaration's own subtree again (nothing further to find) } - const std::uint32_t n = ts_node_child_count( node ); - for( std::uint32_t i = 0; i < n; ++i ) - { - ln_collectLocalDecls( ts_node_child( node, i ), funcRoot, depth - 1, out, defStartLine, defBytes ); - } + // O(children), not O(children²): the re-parsed subtree is a whole DEFINITION, whose body node holds + // one child per statement AND one per comment between them (extras are spliced into the child array — + // src/infra/tschildren.h). A 16 000-comment body measured 15× --lint without --naming-locals before + // this became a cursor (test/childwalkscalecheck.sh, arm B6). The cursor is this frame's own: the + // loop body recurses. + ChildCursor cursor( node ); + forEachChild( node, cursor.cur, [ & ]( TSNode child ) + { ln_collectLocalDecls( child, funcRoot, depth - 1, out, defStartLine, defBytes ); return true; } ); } // collectGatedLocalNames itself (the ingest.h-declared, EXTERNAL-linkage entry point) is defined further diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 9055514dc..7bb241c95 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -1762,18 +1762,21 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t target = importSpecifierText( mn, src ); } const TSNode moduleNode = isFrom ? ts_node_child_by_field_name( stmt, "module_name", 11 ) : TSNode{}; - const std::uint32_t n = ts_node_child_count( stmt ); - for( std::uint32_t i = 0; i < n; ++i ) + // One import statement's clause list: the count comes from the input (`from m import ( a, b, … )`), + // and a comment between two clauses is a further child, so the indexed form was O(children²) here too. + // No scaling arm exists for it (an import flood is not a shape any corpus produces) — this is the + // pure-iteration conversion, covered by the byte-identical arms (test/childwalkscalecheck.sh). + ChildCursor cursor( stmt ); + forEachChild( stmt, cursor.cur, [ & ]( TSNode kid ) { - const TSNode kid = ts_node_child( stmt, i ); if( ts_node_is_null( kid ) ) { - continue; + return true; } const char* kt = ts_node_type( kid ); if( isFrom && ts_node_eq( kid, moduleNode ) ) { - continue; // the module_name child of a from-import is not a bound name; only the `name:` clauses are + return true; // the module_name child of a from-import is not a bound name; only the `name:` clauses are } std::string_view bound; std::string clauseTarget; @@ -1783,7 +1786,7 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t const TSNode nm = ts_node_child_by_field_name( kid, "name", 4 ); if( ts_node_is_null( alias ) || ts_node_is_null( nm ) ) { - continue; + return true; } bound = pattern::nodeText( alias, src ); clauseTarget = isFrom ? target : importSpecifierText( nm, src ); @@ -1805,11 +1808,11 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t } else { - continue; // keywords, punctuation, wildcard_import + return true; // keywords, punctuation, wildcard_import } if( bound.empty() || clauseTarget.empty() ) { - continue; + return true; } RawBind b; b.fileId = fileId; @@ -1819,7 +1822,8 @@ inline void capturePythonImportBinds( TSNode stmt, const char* t, std::uint32_t b.var.assign( bound ); b.typeName = std::move( clauseTarget ); binds.push_back( std::move( b ) ); - } + return true; + } ); } void captureIncludes( TSNode root, Lang lang, std::uint32_t fileId, std::string_view src, std::vector& incs, std::vector& refs, diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index 324ff3104..f2f4dceed 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -172,6 +172,7 @@ void ffiVisitNode( FfiCtx& cx, TSNode n, const char* t ) { // inner DFS: collect the identifier of every function_declarator in the linkage body. std::vector inner; + ChildCursor cursor( n ); // reused across nodes — this walk never recurses inner.push_back( n ); while( !inner.empty() ) { @@ -193,11 +194,12 @@ void ffiVisitNode( FfiCtx& cx, TSNode n, const char* t ) } } } - const std::uint32_t mc = ts_node_child_count( m ); - for( std::uint32_t i = 0; i < mc; ++i ) - { - inner.push_back( ts_node_child( m, i ) ); - } + // O(children), not O(children²): an `extern "C"` block's declaration list is one node + // holding every declaration in it AND every comment between them (extras land in the + // child array — src/infra/tschildren.h). 16 000 of them measured 14× the identical + // flood outside a linkage_specification before this became a cursor + // (test/childwalkscalecheck.sh, arm B5). `inner` IS the work list, so APPEND. + appendChildren( m, cursor.cur, inner ); } } } @@ -471,30 +473,33 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) handlerName.assign( nodeSrc( nameNode ) ); } } - const std::uint32_t cc = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < cc; ++i ) + // decorators of ONE definition: the count comes from the input, and a comment between two + // decorators is a further child, so the indexed form was O(children²) here too. No scaling arm + // exists for it (a decorator flood is not a shape any corpus produces) — this is the + // pure-iteration conversion, covered by the byte-identical arms (test/childwalkscalecheck.sh). + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode dec ) { - const TSNode dec = ts_node_child( n, i ); if( !kindIs( ts_node_type( dec ), "decorator" ) ) { - continue; + return true; } const TSNode expr = ts_node_named_child( dec, 0 ); if( ts_node_is_null( expr ) || !kindIs( ts_node_type( expr ), "call" ) ) { - continue; + return true; } const TSNode fn = ts_node_child_by_field_name( expr, "function", 8 ); if( ts_node_is_null( fn ) || !kindIs( ts_node_type( fn ), "attribute" ) ) { - continue; + return true; } const std::string_view attrName = nodeSrc( ts_node_child_by_field_name( fn, "attribute", 9 ) ); const TSNode argsNode = ts_node_child_by_field_name( expr, "arguments", 9 ); const std::string path = firstPathStringArg( argsNode, src ); if( path.empty() ) { - continue; + return true; } HttpMethod method = HttpMethod::Unknown; @@ -509,11 +514,12 @@ void routesVisitNode( RouteCtx& cx, TSNode n, const char* t ) method = httpMethodFromName( attrName ); if( method == HttpMethod::Unknown ) { - continue; // not a recognized verb shortcut (e.g. .on_event) + return true; // not a recognized verb shortcut (e.g. .on_event) } } routeDefs.push_back( RouteDef{ fileId, ts_node_start_point( n ).row + 1, method, path, handlerName } ); - } + return true; + } ); } // JS/TS: ONE dispatch over every call_expression — client shapes (`fetch`, `axios.`) are // checked FIRST and UNCONDITIONALLY (their callee shape is specific enough to need no file gate), From cd7e0353873c220ec2c025442e42fb3477780a57 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:13:51 -0400 Subject: [PATCH 28/73] =?UTF-8?q?fix(cache):=20one=20root=20key=20for=20ev?= =?UTF-8?q?ery=20cache=20family=20=E2=80=94=20the=20pin=20covers=20the=20w?= =?UTF-8?q?hole=20root,=20not=20half=20of=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1-1 pinned "this root's blobs" through the byte-budget sweep by reading a 16-hex root field off the blob name. It landed with a stated gap: llvm's qchurn blob carried 6b73c58ba5897c7a while the family key was 4280d3ca01d82374, so that family was not pinned. This closes it. WHY qchurn DIVERGED — not a different input, a different CONSTANT. Both builders hash realpath(root) with FNV-1a over the same material, through the same fnv1aAbsorb. They seeded it differently: src/main.cpp::defaultCachePath 1469598103934665603 (17 digits — a TRUNCATED FNV-1a basis) src/quality.h::headSnapRepoHex 14695981039346656037 (the real FNV-1a-64 offset basis) so ONE root minted TWO key families, on every corpus, always. It is not an llvm accident and not a qchurn accident: reproduced on a four-file fixture in one command — ripwire-844a155665d606eb-lean.bin ripwire-qchurn-526f2ad625b9f069--01e462c9dce39b67.bin ripwire-844a155665d606eb-rich.bin ripwire-qsnap-526f2ad625b9f069-…-83137139056a0e11.bin ripwire-qheadsnap-526f2ad625b9f069-…-83137139056a0e11.bin and 844a155665d606eb / 526f2ad625b9f069 are exactly python's fnv1a(realpath, basis) under the two seeds. The split is lean+rich vs EVERY sha-keyed family (qheadsnap, qsnap, qbody, qhist, qms, qchurn, stier), so the pin was covering half the directory. mcpCachePath diverged twice over: the same truncated seed AND no realpath at all, so a trailing slash or a symlinked checkout minted a second MCP blob. THE FIX. `quality::cacheRootKeyHex` is now the ONE derivation (headSnapRepoHex renamed — it keys seven families, not one), and `rootKeyedCachePath` the one builder for the two families whose whole key IS the root; defaultCachePath and mcpCachePath call it instead of open-coding a hash each. realpath-normalized, so symlinks, `.`/`..`, `//` and a trailing '/' all fold; when realpath fails the same folding is done lexically via resolve.h's `lexicalNormalize`. WHICH SEED SURVIVED, AND WHY IT IS THE TRUNCATED ONE. A key change orphans every blob spelled the old way. Taking fnv1a64's basis would rename the MAIN PARSE CACHE — 1.76 GB on llvm alone (rich 1.19 + lean 0.57), a full cold re-parse per root on the machine. Taking defaultCachePath's renames only the git-metadata families: kilobytes, one `git log` walk. The constant is an IDENTITY, not a digest, and FNV-1a's avalanche comes from the prime multiply, so nothing is weaker — only naming compatibility differs, by three orders of magnitude. It is now `kCacheRootKeySeed` with that argument beside it, because "fixing" it back would silently throw away every warm parse cache in existence. NO SCHEME BUMP, deliberately. kQChurnCacheScheme / kQSnapCacheScheme / kHeadSnapCacheScheme exist so a blob whose CONTENT MEANING changed becomes a clean miss. No content changes here — only the root FIELD of the NAME, so every old blob is already never NAMED again, which is what a bump buys. Bumping would assert a content change that did not happen. The old-spelling blobs are ordinary orphans: the "ripwire-" sweep still matches them by prefix and the 30-day age pass deletes them on schedule — verified by seeding a backdated `ripwire-qchurn--….bin` and watching a later run remove it. That pass is silent for EVERY blob it takes (it has no disclosure line at all, by P1-1's design), so an orphan is treated exactly as any other aged-out blob, with no special case either way. TWO FAMILIES ARE NOT ROOT-KEYED, and the llvm run is what surfaced the second one: `ripwire-docmd-` is content-addressed (the document's bytes) and `ripwire-stier-` is FILE-addressed (one span-tier memo per source file above 32 KiB — an llvm --for leaves ~30 of them, each with its own key). Their 16-hex field is real, just not a key over a root, and cacheBlobRootKey was reading it as one. They are now named in `kNonRootKeyedBlobPrefixes` and read as UNOWNED rather than renamed: their names are correct for what they identify, and renaming would orphan the most expensive blob in the directory to rebuild (docmd costs a markitdown popen and a Python start, seconds per file). test/evictioncheck.sh mirrors the list in shell and fails on drift. LLVM-PROJECT, private TMPDIR, real blobs, LLVM_LOCK held 18:59–19:03: E1 --for="how are pass pipelines registered" cold, 138.74 s wall / 290.39 s user / 29.78 s sys -> ripwire-4280d3ca01d82374-rich.bin 1,188,813,175 B -> ripwire-qchurn-4280d3ca01d82374--7a4a….bin 10,226,984 B <- WAS 6b73c58ba5897c7a seed a foreign root's 1500M blob (dir = 2,771,904,159 B, over the 2 GiB budget) E2 --grep=SmallVector cold; its saveCache sweep runs with rich + qchurn already on disk stderr, EXACTLY ONE line: ripwire: cache …: over its 2048 MiB budget — evicted 1 blob(s) of other roots (this root's own families are kept) surviving: rich 1,188,813,175 B + lean 568,688,230 B + qchurn 10,226,984 B — no llvm family evicted. Byte-identical A/B vs lane C's binary (5723b2c0): 12/12 — ripwire tree, golang/go, rocksdb x --top-k=100000, --for, --grep, --pack-task. Determinism (two runs cmp) identical, xmllint clean. --quality-delta: the first cut regressed duplication (mcpCachePath vs defaultCachePath, 54 tokens) and then complexity on cacheBlobRootKey (14 -> 18, bar 15); both were REMOVED by extracting rootKeyedCachePath and isNonRootKeyedBlob rather than acked. What is left is short-horizon-churn on the functions this change must touch, which is the dirty-tree-vs-git-HEAD artifact. Nothing acked. test/fixedbufsweep.sh's census is re-derived, not bumped: main.cpp's `tail` drops from 2 call sites to 1 (defaultCachePath's assembly moved to quality.h::rootKeyedCachePath, and its prose moved with it), quality.h's `tail` rises 1 -> 2 (shaKeyedCachePath's tail[96] plus rootKeyedCachePath's tail[64]), and mcpindex.h's `name` row is deleted because that buffer no longer exists. Those three account exactly for the pinned enumeration going 213 -> 212 calls/sites and 89 -> 88 rows. README.md's recorded --for capture named `headSnapRepoHex` in its prose bullet and in one row. The rename is carried through so the README does not name a symbol the repo no longer has (a reader's --grep would come back empty). Only the IDENTIFIER moved: no measured value in that capture is touched, and its line numbers were already historical before this change. On the committed tree --quality-delta reports regressions="0" gating="0". Co-Authored-By: Claude Fable 5.1 --- README.md | 4 +- src/dmm.h | 2 +- src/gitoracle.h | 2 +- src/ingest_astquery.h | 4 +- src/main.cpp | 21 +++--- src/mcpindex.h | 13 ++-- src/mergescout.h | 2 +- src/quality.h | 155 ++++++++++++++++++++++++++++++++++++------ test/evictioncheck.sh | 26 ++++++- test/fixedbufsweep.sh | 7 +- 10 files changed, 184 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index d157ea9f3..263db34a2 100644 --- a/README.md +++ b/README.md @@ -245,7 +245,7 @@ and the three controls below it. - **Risk, annotated in place** — complexity, git churn (`ingest` shows 128 recent edits), change amplification (touch `ingest` and 266 graph nodes feel it), purity and test coverage. The fragile spots are visible *before* anything touches them. -- **One-hop call context** — `spanTierMemoPath` calls `shaKeyedCachePath`, `headSnapRepoHex`, +- **One-hop call context** — `spanTierMemoPath` calls `shaKeyedCachePath`, `cacheRootKeyHex`, `exclConfigHex`; no second query needed to see the neighbourhood. - **Its own confidence** — this answer says `confidence="high"` with the score margin attached; a flat ranking says `low`, so it reads as a starting point instead of masquerading as an answer. @@ -727,7 +727,7 @@ call a CLI. - + diff --git a/src/dmm.h b/src/dmm.h index c940483cc..55b0315ad 100644 --- a/src/dmm.h +++ b/src/dmm.h @@ -271,7 +271,7 @@ inline bool ingestCommitTree( const std::string& root, const std::string& sha, c std::string cachePath; if( sha == quality::gitHeadSha( root ) ) { - const std::string repoHex = quality::headSnapRepoHex( root ); + const std::string repoHex = quality::cacheRootKeyHex( root ); const std::string exclHex = quality::headSnapExclHex( excludes, maxFileBytes ); cachePath = quality::headSnapCachePath( repoHex, exclHex, sha ); } diff --git a/src/gitoracle.h b/src/gitoracle.h index 4864c3682..3b0db1e06 100644 --- a/src/gitoracle.h +++ b/src/gitoracle.h @@ -241,7 +241,7 @@ inline std::string oracleExclHex() inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) { - return quality::shaKeyedCachePath( "qhist", quality::headSnapRepoHex( root ), oracleExclHex(), headSha ); + return quality::shaKeyedCachePath( "qhist", quality::cacheRootKeyHex( root ), oracleExclHex(), headSha ); } // The fixed-width fields go through quality.h's own POD pair — quality::qsnapPut / quality::qsnapGet, the diff --git a/src/ingest_astquery.h b/src/ingest_astquery.h index 00440c8a8..c994c23bc 100644 --- a/src/ingest_astquery.h +++ b/src/ingest_astquery.h @@ -1302,14 +1302,14 @@ constexpr long long kSpanTierMemoMinBytes = 32ll << 10; // Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex per key // field, then shaKeyedCachePath to assemble and shard the name. Two properties come free and are the reason -// to reuse rather than hand-roll a fourth name builder — headSnapRepoHex realpath-normalizes before hashing, +// to reuse rather than hand-roll a fourth name builder — cacheRootKeyHex realpath-normalizes before hashing, // so two spellings of one file share a blob; and exclConfigHex folds extractionIdentityTag(), so a // kParserVer/kCacheVersion bump renames every memo blob at once, which is the same self-healing invalidation // the parse cache already has. (The hand-rolled fixed-buffer name builder this replaces was flagged as a // 60-token clone of those very builders by --quality-delta, and the detector was right.) inline std::string spanTierMemoPath( const std::string& diskPath ) { - return quality::shaKeyedCachePath( "stier", quality::headSnapRepoHex( diskPath ), + return quality::shaKeyedCachePath( "stier", quality::cacheRootKeyHex( diskPath ), quality::exclConfigHex( {}, "stier" ), std::to_string( kSpanTierMemoVersion ) ); } diff --git a/src/main.cpp b/src/main.cpp index 6d0515249..ba5a8748b 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -161,8 +161,16 @@ using rw::quality::isHeaderPath; // quality.h — the --dead-code elig using rw::quality::sourceHasStaticToken; using rw::quality::deadCodeEligibleKind; -// Warm-by-default cache location: a per-root file keyed by the root's ABSOLUTE path (FNV-1a 64), under -// the hardened cacheDirLadder(), so repeated invocations on the same tree re-parse only changed files. +// Warm-by-default cache location: a per-root file keyed by the root's ABSOLUTE path, under the hardened +// cacheDirLadder(), so repeated invocations on the same tree re-parse only changed files. +// +// The 16-hex root field comes from quality.h's `cacheRootKeyHex` — the ONE canonical spelling every cache +// family now shares (lean/rich here, `ripwire-mcp-.cache`, and shaKeyedCachePath's qheadsnap/qsnap/ +// qbody/qhist/qms/qchurn/stier). This function used to open-code the hash with a TRUNCATED FNV-1a offset +// basis while quality.h used the real one, so one root minted two key families and the byte-budget pin +// (evictBySizeBudget) could only ever see half of them. The seed that survived is this function's, because +// it is the one that keeps the 1.76 GB llvm parse cache warm; the full argument lives beside +// `kCacheRootKeySeed`. // Absolute path so two different dirs both invoked as "." don't collide (a collision would only ever // cost a cold re-parse — the cache is keyed per-file-path internally — but absolute keeps each tree's // cache distinct and warm). Cache content is content-hashed + parserVer-gated, so a stale/foreign cache @@ -194,14 +202,7 @@ using rw::quality::deadCodeEligibleKind; // and can no longer truncate the shared blob. Gate: test/cacheoffsetcheck.sh. std::string defaultCachePath( const std::string& root, bool captureValueUses ) { - char absbuf[ PATH_MAX ]; - const char* abs = realpath( root.c_str(), absbuf ) ? absbuf : root.c_str(); - std::uint64_t h = 1469598103934665603ull; - for( const char* c = abs; *c; ++c ) { h ^= static_cast( *c ); h = rw::hashutil::fnv1aMultiply( h ); } - char tail[ 48 ]; - rw::formatTo( tail, sizeof( tail ), "ripwire-{:016x}-{}.bin", - static_cast( h ), captureValueUses ? "rich" : "lean" ); - return resolveCacheBlobPath( cacheDirLadder(), tail ); + return rw::quality::rootKeyedCachePath( root, "ripwire-", captureValueUses ? "-rich.bin" : "-lean.bin" ); } // computeHeadSnapshot / gitHeadSha / gitRepoHasHistory / cacheDirLadder now live in quality.h (the diff --git a/src/mcpindex.h b/src/mcpindex.h index e53470b08..397274700 100644 --- a/src/mcpindex.h +++ b/src/mcpindex.h @@ -584,14 +584,15 @@ struct McpIndex // Cache file path, deterministic per (user, root), under the shared private cache ladder and its existing // two-hex shard layout. MCP sessions used to leave one flat file per temporary checkout directly in TMPDIR; // tens of thousands of those files made every later cache-hygiene scan enumerate the shared directory. +// +// The root field is `quality::cacheRootKeyHex` — the ONE canonical spelling the CLI families use, so an MCP +// blob is pinned by the byte-budget sweep alongside its own root's lean/rich/qchurn siblings instead of +// looking like a foreign root. This used to open-code the hash AND skip realpath entirely, so the MCP blob +// diverged from the CLI's twice over: a different offset basis, and a key that followed the SPELLING of the +// root (a trailing slash or a symlinked checkout minted a second blob). inline std::string mcpCachePath( const std::string& root ) { - std::uint64_t h = 1469598103934665603ULL; // FNV-1a of the root → a stable per-root cache name - for( char c : root ) { h ^= static_cast( c ); h = hashutil::fnv1aMultiply( h ); } - char name[ 64 ]; - rw::formatTo( name, sizeof( name ), "ripwire-mcp-{:016x}.cache", (unsigned long long)h ); - - return quality::resolveCacheBlobPath( quality::cacheDirLadder(), name ); + return quality::rootKeyedCachePath( root, "ripwire-mcp-", ".cache" ); } // working-set (Cody-style): FNV-1a-64 of the SORTED changed-file id list, so the hash is a pure diff --git a/src/mergescout.h b/src/mergescout.h index 287e851ef..6779969b5 100644 --- a/src/mergescout.h +++ b/src/mergescout.h @@ -344,7 +344,7 @@ class TreeIndexMemo public: TreeIndexMemo( const std::string& root, const std::vector& excludes, std::size_t maxFileBytes ) : root_( root ), excludes_( excludes ), maxFileBytes_( maxFileBytes ), - repoHex_( quality::headSnapRepoHex( root ) ), exclHex_( msExclHex( excludes ) ) {} + repoHex_( quality::cacheRootKeyHex( root ) ), exclHex_( msExclHex( excludes ) ) {} // Register one future get(sha) BEFORE the diff loop runs — see the class comment above. void reserve( const std::string& sha ) { ++pending_[ sha ]; } diff --git a/src/quality.h b/src/quality.h index d61b98974..9f00a6862 100644 --- a/src/quality.h +++ b/src/quality.h @@ -1356,19 +1356,83 @@ inline bool gitRepoHasHistory( const std::string& root ) // header already self-validates). Folded into the filename key so an old-scheme file is simply never named. constexpr std::uint32_t kHeadSnapCacheScheme = 1; -// The 16-hex repo key: fnv1a64 of realpath(root) (matching defaultCachePath) so two spellings of one repo -// share one warm cache AND one eviction group. A null realpath (missing path) degrades to the verbatim -// spelling — still correct, at worst one extra cold miss. Used by both the filename and the eviction glob. -inline std::string headSnapRepoHex( const std::string& root ) -{ - char* rp = ::realpath( root.c_str(), nullptr ); - const std::string absRoot = rp ? std::string( rp ) : root; - if( rp ) - { +// ─── THE ROOT KEY — one canonical spelling, for every cache family ──────────────────────────────────── +// +// The 16-hex field every cache blob's filename carries, identifying the ROOT the blob belongs to: +// `ripwire--{lean,rich}.bin` (main.cpp::defaultCachePath), `ripwire-mcp-.cache` +// (mcpindex.h::mcpCachePath) and `ripwire----.bin` +// (shaKeyedCachePath below: qheadsnap, qsnap, qbody, qhist, qms, qchurn, stier). It is what makes +// "which root does this blob belong to?" answerable from the NAME alone — see cacheBlobRootKey and the +// byte-budget pin in evictBySizeBudget, which is only ever as wide as the set of blobs that spell the +// key the SAME way. +// +// AND TWO SPELLINGS SHIPPED. Both builders hashed realpath(root) with FNV-1a, but with DIFFERENT offset +// bases: `defaultCachePath` (and `mcpCachePath`) seeded 1469598103934665603 — seventeen digits, a +// TRUNCATED FNV-1a-64 basis — while this function seeded arch.h's `fnv1a64`, i.e. the real +// 14695981039346656037. Same material, two keys, on every root, always. Measured on llvm-project: +// lean/rich carried 4280d3ca01d82374 while qchurn carried 6b73c58ba5897c7a; reproduced on a four-file +// fixture as `ripwire-844a155665d606eb-{lean,rich}.bin` beside `ripwire-qchurn-526f2ad625b9f069--….bin`. +// The consequence is exactly the gap P1-1 stated: the pin covered lean+rich and left every git-metadata +// family evictable by the very root that had just written it. +// +// WHY THE SURVIVING BASIS IS THE TRUNCATED ONE, AND WHY IT MUST NOT BE "FIXED". A key change orphans +// every blob spelled the old way. Adopting `fnv1a64`'s basis would have renamed the MAIN PARSE CACHE — +// 1.76 GB of it on llvm-project alone (rich 1.19 GB + lean 0.57 GB), a full cold re-parse for every root +// on the machine. Adopting defaultCachePath's renames only the git-metadata families, which are +// kilobytes and rebuild from one `git log` walk. The constant is an IDENTITY, not a digest: FNV-1a's +// avalanche comes from the prime multiply, and any odd seed gives the same distribution over these +// inputs, so nothing is weaker — only the naming compatibility differs, and it differs by three orders +// of magnitude. Changing `kCacheRootKeySeed` to the textbook basis would silently throw away every warm +// parse cache in existence; test/evictioncheck.sh (k) is what makes such a change visible, but it will +// go GREEN on a uniform wrong seed, so this paragraph is the guard. +// +// NORMALIZED, so the key follows the TREE and not its spelling: `realpath` collapses symlinks, `.`/`..`, +// `//` and a trailing '/'. When realpath fails — the path does not exist, so there is nothing to cache +// under it anyway — the same folding is done LEXICALLY (resolve.h's `lexicalNormalize`, the house's +// segment-stack folder) so that at least the trailing-slash and `.`/`..` cases still agree; an unsound +// `..` escape yields "" there and degrades to the verbatim spelling, still correct, at worst one extra +// cold miss. Gate: test/evictioncheck.sh (k) one root ⇒ one key across every family, (l) a trailing +// slash and a symlinked spelling add no new key. +// +// NO SCHEME BUMP, AND THE REASON IS THE HOUSE RULE ITSELF, NOT AN OMISSION. `kQChurnCacheScheme`, +// `kQSnapCacheScheme` and `kHeadSnapCacheScheme` exist so that a blob whose CONTENT MEANING changed +// becomes a clean miss rather than a wrong answer served from cache (see kQChurnCacheScheme's own comment: +// scheme 2 was a merge-blind stream). Nothing about any blob's content changes here — only the root FIELD +// of its NAME. Every pre-existing blob is therefore already never NAMED again, which is precisely the +// effect a bump buys, reached by the key rather than by a version. Bumping on top would assert a content +// change that did not happen, and would additionally invalidate the blobs that are about to be re-minted +// under the unified key anyway. The old-spelling blobs are ordinary orphans: the "ripwire-" family sweep +// still matches them by prefix, so the 30-day age pass deletes them on schedule — verified by seeding one +// backdated `ripwire-qchurn--…bin` and watching a later run remove it. That pass is silent for +// every blob it takes (it has no disclosure line at all — see evictBySizeBudget's note on why only the +// byte-budget pass speaks), so an orphan is treated exactly as any other aged-out blob, with no special +// case in either direction. +inline constexpr std::uint64_t kCacheRootKeySeed = 1469598103934665603ull; + +inline std::string cacheRootKeyHex( const std::string& root ) +{ + char* rp = ::realpath( root.c_str(), nullptr ); + std::string absRoot; + if( rp != nullptr ) + { + absRoot = rp; std::free( rp ); } + else + { + absRoot = lexicalNormalize( root ); + if( absRoot.empty() ) + { + absRoot = root; // a `..` that escapes above its own base — unsound to fold, hash it verbatim + } + } + std::uint64_t h = kCacheRootKeySeed; + for( const char c : absRoot ) + { + h = rw::hashutil::fnv1aAbsorb( h, c ); + } char hex[ 20 ]; - rw::formatTo( hex, sizeof( hex ), "{:016x}", static_cast( fnv1a64( absRoot ) ) ); + rw::formatTo( hex, sizeof( hex ), "{:016x}", static_cast( h ) ); return std::string( hex ); } @@ -1642,18 +1706,60 @@ inline std::string headSnapCachePath( const std::string& repoHex, const std::str return shaKeyedCachePath( "qheadsnap", repoHex, exclHex, headSha ); } +// The builder for the two families whose whole key IS the root — the main parse cache +// (`ripwire--lean.bin` / `-rich.bin`, main.cpp::defaultCachePath) and the MCP index +// (`ripwire-mcp-.cache`, mcpindex.h::mcpCachePath). They sat in different translation units and +// each open-coded the same three lines around its own copy of the hash, which is exactly how the two root +// spellings drifted apart in the first place; one body means a future family joins by naming a prefix and +// a suffix rather than by re-deriving a key. `prefix`/`suffix` bracket the 16-hex field because that is the +// only thing the two shapes disagree about — everything the pin reads is in the middle. +inline std::string rootKeyedCachePath( const std::string& root, const char* prefix, const char* suffix ) +{ + char tail[ 64 ]; + rw::formatTo( tail, sizeof( tail ), "{}{}{}", prefix, cacheRootKeyHex( root ).c_str(), suffix ); + return resolveCacheBlobPath( cacheDirLadder(), tail ); +} + // P1-1 (2026-09-10 full audit) — THE PIN KEY. Every cache blob's filename carries the SAME 16-hex root -// field: `defaultCachePath` writes `ripwire--{lean,rich}.bin` and `shaKeyedCachePath` writes -// `ripwire----.bin`, and `headSnapRepoHex` above hashes exactly the -// material `defaultCachePath` does (fnv1a64 of realpath(root)), so ONE root's every family — lean, rich, -// qheadsnap, qsnap, qbody, qhist, qms, qchurn, stier — spells the same key in the same place. That makes -// "which root does this blob belong to?" answerable from the NAME alone, with no plumbing: the byte-budget -// sweep reads the key off the very blob it is about to write (`keepPath`) and pins its siblings. +// field, and since the follow-up round it really is the same one: `defaultCachePath` writes +// `ripwire--{lean,rich}.bin`, `mcpCachePath` writes `ripwire-mcp-.cache` and +// `shaKeyedCachePath` writes `ripwire----.bin`, all three through the ONE +// canonical `cacheRootKeyHex` above — so ONE root's every family (lean, rich, mcp, qheadsnap, qsnap, qbody, +// qhist, qms, qchurn, stier) spells the same key in the same place. That makes "which root does this blob +// belong to?" answerable from the NAME alone, with no plumbing: the byte-budget sweep reads the key off the +// very blob it is about to write (`keepPath`) and pins its siblings. // // The rule is positional-free on purpose: return the FIRST '-'-delimited field that is exactly 16 hex // digits. No family tag is 16 characters of hex ("qheadsnap", "qsnap", "qbody", "qhist", "qms", "qchurn", -// "stier"), so the first such field is the root key in BOTH filename shapes, and a foreign or legacy blob -// that carries no such field yields "" — which pins nothing and evicts exactly as it did before. +// "stier", "mcp"), so the first such field is the root key in every filename shape, and a foreign or legacy +// blob that carries no such field yields "" — which pins nothing and evicts exactly as it did before. +// +// TWO FAMILIES ARE EXCEPTIONS, and they are NAMED rather than guessed at — their 16-hex field is a real +// key, just not a key over a ROOT: +// * `ripwire-docmd-.bin` (ingest_docpass.h) is CONTENT-addressed: fnv1a64 of the DOCUMENT'S +// BYTES, so one PDF extracted under two checkouts is cached once. +// * `ripwire-stier--…` (ingest_astquery.h::spanTierMemoPath) is FILE-addressed: it passes a +// per-file disk path to cacheRootKeyHex, one memo per source file above a 32 KiB floor. An llvm --for +// leaves ~30 of them beside the three root-keyed blobs, each with its own key. +// Reading either as a root key would be a wrong answer about OWNERSHIP — it names a document or a file, +// not the tree the blob belongs to — and at 1-in-2^64 could pin a blob to an unrelated root. Both are +// excluded here rather than renamed: their names are correct for what they identify, and renaming would +// orphan the most expensive thing in this directory to rebuild (a docmd blob costs a markitdown popen and +// a Python start, seconds per file). They yield "" and are treated as unowned, which is what they are — +// the byte-budget sweep may take them, and that is the right policy for a per-file memo whose recompute +// cost is one file, not one tree. +// +// KEEP THE LIST HONEST: test/evictioncheck.sh arm (k) mirrors these prefixes in shell and FAILS if the two +// lists disagree, so a family added later with a non-root 16-hex field is a gate failure rather than a +// blob quietly pinned to a stranger. +inline constexpr std::string_view kNonRootKeyedBlobPrefixes[] = { "ripwire-docmd-", "ripwire-stier-" }; + +inline bool isNonRootKeyedBlob( std::string_view blobName ) noexcept +{ + return std::any_of( std::begin( kNonRootKeyedBlobPrefixes ), std::end( kNonRootKeyedBlobPrefixes ), + [ blobName ]( const std::string_view prefix ) noexcept { return blobName.starts_with( prefix ); } ); +} + inline std::string cacheBlobRootKey( std::string_view blobName ) noexcept { const auto isHex16 = []( std::string_view f ) noexcept @@ -1672,6 +1778,11 @@ inline std::string cacheBlobRootKey( std::string_view blobName ) noexcept return true; }; + if( isNonRootKeyedBlob( blobName ) ) + { + return std::string{}; // content- or file-addressed, root-independent by design — see above + } + std::size_t at = 0; while( at < blobName.size() ) { @@ -2642,7 +2753,7 @@ inline std::pair computeHeadSnapshot( const std::string& root, c // Cache keys, computed ONCE and shared by both the Snapshot cache (this step) and the ingest cache (step 3). const std::string headSha = gitHeadSha( root ); // non-empty: gitRepoHasHistory passed above const bool useCache = !headSha.empty(); - const std::string repoHex = useCache ? headSnapRepoHex( root ) : std::string{}; + const std::string repoHex = useCache ? cacheRootKeyHex( root ) : std::string{}; const std::string exclHex = useCache ? headSnapExclHex( excludes, maxFileBytes ) : std::string{}; // ingest-cache family const std::string qExclHex = useCache ? qsnapExclHex( excludes, maxFileBytes ) : std::string{}; // Snapshot-cache family const std::string qsnapPath = useCache ? qsnapCachePath( repoHex, qExclHex, headSha ) : std::string{}; @@ -2778,7 +2889,7 @@ inline bool loadRefTree( const std::string& repoRoot, const std::string& sha, co std::string cachePath; if( sha == gitHeadSha( repoRoot ) ) { - cachePath = headSnapCachePath( headSnapRepoHex( repoRoot ), headSnapExclHex( excludes, maxFileBytes ), sha ); + cachePath = headSnapCachePath( cacheRootKeyHex( repoRoot ), headSnapExclHex( excludes, maxFileBytes ), sha ); } { @@ -2820,7 +2931,7 @@ computeWindowRefBodyHashes( const std::string& root, std::uint32_t days, return { {}, false }; } - const std::string repoHex = headSnapRepoHex( root ); + const std::string repoHex = cacheRootKeyHex( root ); const std::string exclHex = headSnapExclHex( excludes, maxFileBytes ); // ingest-cache family (shared with qheadsnap) const std::string qbExclHex = qbodyExclHex( excludes, maxFileBytes ); const std::string qbodyPath = qbodyCachePath( repoHex, qbExclHex, refSha ); @@ -3031,7 +3142,7 @@ inline std::vector> gitCoChangeAndChurnCached( return resolveCommitStream( gitLogNameOnlyRaw( root, coSince ), ing, maxFiles, churnCutoff, outChurn, onlyRoot ); } - const std::string repoHex = headSnapRepoHex( root ); + const std::string repoHex = cacheRootKeyHex( root ); const std::string boundary = gitWindowBoundarySha( root, coSince ); // cheap — no --name-only std::string keyMat = headSha; keyMat.push_back( '\x1f' ); keyMat += coSince; diff --git a/test/evictioncheck.sh b/test/evictioncheck.sh index e93dfff17..d2ac1119b 100755 --- a/test/evictioncheck.sh +++ b/test/evictioncheck.sh @@ -391,7 +391,7 @@ rc6=$? # blobs that SPELL the root the same way, and two spellings shipped. Both builders hash realpath(root) with # FNV-1a, but with DIFFERENT offset bases: # main.cpp::defaultCachePath seeded 1469598103934665603 (17 digits — a truncated basis) -# quality.h::headSnapRepoHex seeded 14695981039346656037 (the real FNV-1a 64 basis) +# quality.h::cacheRootKeyHex seeded 14695981039346656037 (the real FNV-1a 64 basis) # so ONE root produced TWO key families, always, on every corpus. Measured on llvm-project: lean/rich carried # 4280d3ca01d82374 while qchurn carried 6b73c58ba5897c7a. Reproduced on a four-file fixture in one command: # `ripwire-844a155665d606eb-{lean,rich}.bin` beside `ripwire-qchurn-526f2ad625b9f069--….bin`. Consequence: @@ -413,10 +413,22 @@ if ! command -v git >/dev/null 2>&1; then no "(k)(l) git is required to prime the qchurn/qheadsnap/qsnap families — cannot run" else +# TWO families carry a 16-hex field that is NOT a root key and must be read as unowned: `ripwire-docmd-` +# is content-addressed (the document's bytes) and `ripwire-stier-` is file-addressed (one span-tier memo +# per source file above 32 KiB — an llvm --for leaves ~30 of them). quality.h names them in +# `kNonRootKeyedBlobPrefixes`; this list is the shell mirror, and the arm below FAILS if the two disagree, +# so a family added later with a non-root 16-hex field cannot quietly join the pin. +NONROOT_PREFIXES='ripwire-docmd- ripwire-stier-' + # the root field, by cacheBlobRootKey's own rule: FIRST '-'-delimited field of the basename that is -# exactly 16 hex digits. Prints nothing for a blob that carries no such field. +# exactly 16 hex digits, EXCEPT for the non-root-keyed families above. Prints nothing when there is none. blobrootkey(){ - basename "$1" | sed -E 's/\.(bin|cache)$//' | awk -F- '{ for( i = 1; i <= NF; ++i ) if( $i ~ /^[0-9a-f]{16}$/ ) { print $i; exit } }' + local b p + b="$( basename "$1" )" + for p in $NONROOT_PREFIXES; do + case "$b" in "$p"*) return 0;; esac + done + printf '%s' "$b" | sed -E 's/\.(bin|cache)$//' | awk -F- '{ for( i = 1; i <= NF; ++i ) if( $i ~ /^[0-9a-f]{16}$/ ) { print $i; exit } }' } # every distinct root key present under a cache dir, sorted+uniqued allrootkeys(){ @@ -435,6 +447,14 @@ primeallfamilies(){ env -u XDG_CACHE_HOME TMPDIR="$cb" "$BIN" "$rt" --cochange=f.cpp >/dev/null 2>&1 } +# the two lists must name the SAME families, or this arm reads a key the binary does not. +SRC_NONROOT="$( sed -n 's/^inline constexpr std::string_view kNonRootKeyedBlobPrefixes\[\] = {\(.*\)};$/\1/p' "$ROOT/src/quality.h" \ + | tr ',' '\n' | sed -E 's/[^"]*"([^"]*)".*/\1/' | grep . | sort | tr '\n' ' ' )" +WANT_NONROOT="$( printf '%s\n' $NONROOT_PREFIXES | sort | tr '\n' ' ' )" +[ -n "$SRC_NONROOT" ] && [ "$SRC_NONROOT" = "$WANT_NONROOT" ] \ + && ok "(k) the non-root-keyed family list matches quality.h::kNonRootKeyedBlobPrefixes ($WANT_NONROOT)" \ + || no "(k) family-list drift: quality.h says '$SRC_NONROOT', this gate reads '$WANT_NONROOT'" + TMP6="$( mktemp -d )"; trap 'rm -rf "$TMP" "$TMP2" "$TMP3" "$TMP4" "$TMP5" "$TMP6"' EXIT CB6="$TMP6/cachebase"; CD6="$CB6/ripwire"; mkdir -p "$CD6" R6="$TMP6/repo"; mkdir -p "$R6" diff --git a/test/fixedbufsweep.sh b/test/fixedbufsweep.sh index b38e0664e..3ad82c160 100755 --- a/test/fixedbufsweep.sh +++ b/test/fixedbufsweep.sh @@ -147,7 +147,7 @@ TABLE = { # ── src/lanes.h — THE REFERENCE SAFE SHAPE ─────────────────────────────────────────────────────────── ( "src/lanes.h", "buf" ): ( 10, "safe", "buf[640] x3: snprintf-THEN-escape. :723 interpolates an UNBOUNDED file path and is still safe for exactly that reason — the warning text is escaped downstream, so a cut shortens prose and can never land inside markup. This is the shape §B14's six were not." ), # ── src/main.cpp ───────────────────────────────────────────────────────────────────────────────────── - ( "src/main.cpp", "tail" ): ( 2, "not-markup", "tail[48]: the cache FILENAME ('rich'/'lean' + a %016llx). Bounded and never emitted." ), + ( "src/main.cpp", "tail" ): ( 1, "not-markup", "tail[48]: the shallow-clone cache DIR suffix (\"/ripwire-remote-\" + a fixed-width 16-hex). Bounded and never emitted. Was 2 sites: defaultCachePath's cache FILENAME left this buffer when the root-key unification moved its assembly into quality.h::rootKeyedCachePath, which is where its row now lives." ), ( "src/verbs_for.h", "nb" ): ( 14, "safe", "nb[160] x2: the mention/doc-mention/siblift/expand header notes. Every %s is the plural '' or 's'; everything else is %u." ), ( "src/verbs_report.h", "exemptAttr" ): ( 1, "safe", "exemptAttr[40]: ' exempt=\"%s\"' with groupExemptKind's fixed vocabulary (longest 'fixture' = 7 B, total 19 B)." ), ( "src/verbs_report.h", "hdr" ): ( 1, "safe", "hdr[512]: runSkipped's root (§L1). 175 B of literal + ELEVEN %zu/%llu counters at 20 B worst case = 395 B, plus the ONE %s, which is the compile-time literal ' rows_capped=\"1\"' or '' (18 B) = 413 B against 511 usable. No path, no name, nothing user-supplied reaches this buffer — every emitted path goes through escapeXml straight into the writer, outside it." ), @@ -163,7 +163,7 @@ TABLE = { # ── src/prcontext.h ────────────────────────────────────────────────────────────────────────────────── ( "src/prcontext.h", "tail" ): ( 1, "latent", "tail[256]: truncated=\"%s\" is ESCAPE-THEN-SNPRINTF in shape, but the value is bounded — kPrTrims[].dropped is a const table (longest 48 B) plus ';budget-floor-exceeded' (22 B), none of which escapes. Worst case 88 lit + 90 digits + 70 = 248 B + NUL against 256: SEVEN bytes of margin. A fifth trim level or one more attribute crosses it." ), # ── src/quality.h ──────────────────────────────────────────────────────────────────────────────────── - ( "src/quality.h", "tail" ): ( 1, "not-markup", "tail[96]: the qsnap/qheadsnap cache FILENAME; family + two hex digests + %016llx, all fixed-width." ), + ( "src/quality.h", "tail" ): ( 2, "not-markup", "tail[96] in shaKeyedCachePath: the qsnap/qheadsnap cache FILENAME; family + two hex digests + %016llx, all fixed-width. tail[64] in rootKeyedCachePath: the lean/rich + mcp cache FILENAME, a literal prefix + the 16-hex root key + a literal suffix — every part a compile-time or fixed-width constant. Neither is emitted." ), # ── src/serialize.h ────────────────────────────────────────────────────────────────────────────────── ( "src/serialize.h", "fitAttr" ): ( 1, "safe", "fitAttr[96]: two %zu plus the literal ' over_ceiling=1'." ), ( "src/serialize.h", "attr" ): ( 2, "safe", "attr[352] x2: the per-symbol metric attrs. Widest 26 lit + 4x10 digits + 11 role + qbuf(<=95) + ambs(<=35: amb= + lpin=) + kbuf(<=23) = 230 B." ), @@ -218,7 +218,6 @@ NUMERIC_ONLY = { ( "src/mcpedit.h", "name" ): 1, ( "src/mcpedit.h", "oldStamp" ): 1, ( "src/mcpindex.h", "buf" ): 1, - ( "src/mcpindex.h", "name" ): 1, ( "src/mcpverbs.h", "connectCeiling" ): 1, ( "src/mcpverbs.h", "d" ): 1, ( "src/mcpverbs.h", "deg" ): 1, @@ -397,7 +396,7 @@ if not bad: # the same buffer is now written from six sites rather than three. formatToRuntime is gone with # them: it was the only format in the tree not checked at compile time, and the only one that # could fail at runtime and return an empty buffer. -EXPECTED = { "mentions": 316, "calls": 213, "sites": 213, "rows": 89, "widthforms": 0 } +EXPECTED = { "mentions": 315, "calls": 212, "sites": 212, "rows": 88, "widthforms": 0 } # 2026-09-04 (capture-audit L6, H9): +1 call/+1 mention, sites/rows UNCHANGED — re-read, not # re-counted. packConnect gained ONE snprintf into a new `char connectCeiling[32]` for the # H9 ` max_tokens="%d"` ceiling disclosure: a single %d of a caller-supplied INTEGER, no %s, From f0597f1c6b14bff488739db8da39b7c34c091747 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:24:59 -0400 Subject: [PATCH 29/73] quality(error-masking): a comment is not a handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error-masking kind fired ZERO times across 52 replayed documents and once in 1,177 committed acks. Not because this repo swallows no errors — because all seven of its rules require a LITERALLY empty block: `errorMaskBlockIsEmpty` strips whitespace and compares the collapsed text to `"{}"`. Audit lane Q1's synthetic S2 (`catch(...){}`) was caught; S2b, `catch( const std::exception& ) { /* ignore */ }`, walked straight past — and the comment is where a deliberate swallow is most likely to be WRITTEN DOWN. The one spelling the kind could not see is the one a person reaches for when they mean it. A block whose only content is a comment counts now. `//`, `/* … */` and `#` all open one; a ';' or a '{' anywhere inside means a statement survives and the block is not a swallow, which is what keeps `catch { std::fprintf( stderr, "bad" ); return -2; }` out. MEASURED, and this is the whole point of shipping it: +0 rows over the 40-commit ref-pair replay and +0 over the 12-commit working-tree replay. The two recorded document sets are BYTE-IDENTICAL to the previous dial's, per-commit and row for row (117 rows / 23 gating / 11 of 40, and 208 / 32 / 8 of 12). This widening finds nothing in this history; it turns a synthetic miss into a reported row and costs nothing. TWO FLOORS, written beside the code. astQuery truncates the captured span at 120 characters, so a comment-only block longer than that does not end in '}' here and is not recognized — a miss, never a false hit. And the scan is over flattened text, so a semicolon inside the comment PROSE also keeps the block out. Both directions of the imprecision lose recall rather than manufacturing a finding, which is the only acceptable direction for a kind whose output accuses code of hiding an error. WITHHELD, with the reason. Q1's dial table also proposed widening kErrorMaskRules to a `catch` that only LOGS and to a dropped `std::error_code`. Neither is a query-table addition: "only logs" is a judgement about a block's whole statement list, and "never tested" is data flow. Both need real analysis and, more to the point, a noise measurement that does not exist yet — and a kind that has never produced a false positive is the worst possible place to guess. Recorded in the lane report rather than shipped half-built. GATE: test/qddialscheck.sh §6 — one fixture, two catch blocks, one comment-only and one that logs and returns. RED on the pre-change binary for the comment-only block; the logging block must stay silent on both. lintrulescheck, lintcheck, lintcatalogcheck, lintprecisioncheck, qualitykindscheck (whose Python `except: pass` arm exercises the same predicate): PASS. Co-Authored-By: Claude Fable 5.1 --- src/lintrules.h | 54 ++++++++++++++++++++++++++++++++++++++++++-- test/qddialscheck.sh | 39 ++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/src/lintrules.h b/src/lintrules.h index a506dccce..a84223c9d 100644 --- a/src/lintrules.h +++ b/src/lintrules.h @@ -1060,9 +1060,24 @@ inline constexpr std::array kErrorMaskRules = { { { "(call_expression function: (member_expression property: (property_identifier) @p (#eq? @p \"then\")) arguments: (arguments (_) (arrow_function body: (statement_block) @m)))", "swallow-then-arrow", true }, // .then(_, ()=>{}) } } ; -// Is the collapsed source of a captured block "empty" — only braces and whitespace? astQuery returns the +// Does the captured block SWALLOW — is there nothing in it that could handle the error? astQuery returns the // @m span text with \n/\r/\t already flattened to spaces and truncated to 120 chars; an empty `{}` (even // `{ }` / `{ }`) is far under 120, so the collapsed check is exact for the shapes we target. Deterministic. +// +// Q-DIAL-6 (2026-09-10) — A COMMENT IS NOT A HANDLER. This asked one question, "is the collapsed text exactly +// {}", and audit lane Q1's synthetic S2b — `catch( const std::exception& ) { /* ignore */ }` — walked straight +// past it, as does every `// intentionally ignored`. The comment is where the intent is WRITTEN DOWN; it is +// the most likely spelling of a deliberate swallow, and it was the one spelling the kind could not see. A +// block whose only content is a comment counts. Measured on 40 replayed commits of this repo: +0 rows — the +// widening finds nothing in this history and turns S2b from a silent miss into a reported row. +// +// TWO FLOORS, stated. (1) astQuery truncates the span at 120 characters, so a comment-only block longer than +// that does not end in '}' here and is not recognized — a miss, never a false hit. (2) The scan is over +// flattened text, so a ';' or a '{' anywhere inside means "a statement survives" and the block is not a +// swallow, which is what keeps `catch { log( x ); }` out; a semicolon inside the comment PROSE therefore also +// keeps the block out. Both directions of the imprecision lose recall rather than manufacturing a finding. +// The @p capture filter in findErrorMasking depends on a bare identifier ("catch"/"then") answering false +// here, and it still does: no braces, no match. inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept { std::string stripped; @@ -1073,7 +1088,42 @@ inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept stripped.push_back( c ); } } - return stripped == "{}"; + if( stripped == "{}" ) + { + return true; + } + std::string_view t = collapsed; + while( !t.empty() && ( t.front() == ' ' || t.front() == '\t' ) ) { t.remove_prefix( 1 ); } + while( !t.empty() && ( t.back() == ' ' || t.back() == '\t' ) ) { t.remove_suffix( 1 ); } + if( t.size() < 2 || t.front() != '{' || t.back() != '}' ) + { + return false; + } + const std::string_view mid = t.substr( 1, t.size() - 2 ); + if( mid.find( ';' ) != std::string_view::npos || mid.find( '{' ) != std::string_view::npos ) + { + return false; // a statement survives inside it — not a swallow + } + const std::size_t slash = mid.find( "//" ); + const std::size_t block = mid.find( "/*" ); + const std::size_t hash = mid.find( '#' ); + std::size_t first = std::string_view::npos; + for( std::size_t c : { slash, block, hash } ) + { + if( c != std::string_view::npos && ( first == std::string_view::npos || c < first ) ) { first = c; } + } + if( first == std::string_view::npos ) + { + return false; // content that is not a comment at all + } + for( std::size_t i = 0; i < first; ++i ) + { + if( mid[i] != ' ' && mid[i] != '\t' ) + { + return false; // something precedes the comment + } + } + return true; } // One error-masking hit: the suppressing block's file + start byte (so a caller can attribute it to the diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 42a8c63e7..4df8798a6 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -324,5 +324,44 @@ rm -f "$DP/.ripwire_config" && ok "duplication: byte-identical run to run (deterministic)" || no "duplication: non-deterministic delta" +# ── 6) error-masking: a block whose only content is a COMMENT is a swallow ─────────────────────────────── +# The kind fired ZERO times across 52 replayed documents and once in 1,177 committed acks, because all seven +# of its rules require a LITERALLY empty block. Synthetic S2 (`catch(...){}`) was caught; S2b +# (`catch( const std::exception& ) { /* ignore */ }`) was missed — and the comment is where a deliberate +# swallow is most likely to be written down. The widening is measured at +0 rows over 40 replayed commits. +EM="$WORK/mask"; mkdir -p "$EM/src" +( cd "$EM" && git init -q && git config user.email t@t && git config user.name t && git config commit.gpgsign false ) +cat > "$EM/src/m.cpp" <<'CPP' +#include +#include +int risky( int n ); +int guarded( int n ){ + try { return risky( n ); } + catch( const std::runtime_error& e ) { return -1; } +} +int logged( int n ){ + try { return risky( n ); } + catch( const std::runtime_error& e ) { std::fprintf( stderr, "bad" ); return -2; } +} +CPP +( cd "$EM" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) +python3 - "$EM/src/m.cpp" <<'PY' +import sys +p=sys.argv[1]; s=open(p).read() +s=s.replace("catch( const std::runtime_error& e ) { return -1; }", + "catch( const std::runtime_error& e ) { /* deliberately ignored */ }") +open(p,"w").write(s) +PY +OEM="$( cd "$EM" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" +row "$OEM" error-masking guarded >/dev/null \ + && ok "error-masking: a comment-only catch block is a swallow (synthetic S2b)" \ + || { no "error-masking: the comment-only catch block was missed"; rows "$OEM"; } +row "$OEM" error-masking logged >/dev/null \ + && { no "error-masking: a catch that LOGS and returns was counted — a statement survives in it"; rows "$OEM"; } \ + || ok "error-masking: a catch carrying a real statement is not a swallow" +[ "$OEM" = "$( cd "$EM" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ + && ok "error-masking: byte-identical run to run (deterministic)" || no "error-masking: non-deterministic delta" + + [ "$fail" = 0 ] && echo "qddialscheck: ALL PASS" || echo "qddialscheck: FAILURES" exit "$fail" From c7e9308980d86f2d442b595482b16084cf1533e3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:25:15 -0400 Subject: [PATCH 30/73] docs(quality-bar): name the flag that cuts the checkpoint report by 84% MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--legend=compact` already exists, and nothing that tells an agent to run `--quality-delta` mentions it. CLAUDE.md's Verify block and the ripwire-quality-bar skill both mandate the verb at every "done" moment; neither named the flag that makes it cheap. Measured with this branch's binary: a clean two-function checkpoint goes 2,776 B to 454 B (-83.6%), and this repo mid-change goes 15,601 B to 8,775 B (-43.8%). The rows are byte-identical either way — only the dictionary in front of them is shorter, and an agent that has read it once does not need it on every iteration of a refine loop. This is the cheapest item in Q1's whole report: no code, no gate, a measured 84% saving on the run CLAUDE.md tells every agent to make. WITHHELD, with the reason. Q1-12 also proposed folding the `` stale-ack rows to a count when regressions="0" (up to 124 of them on a clean report; the live ledger makes the mean document 2.8x larger). It is not shipped, and the argument against it is stronger than the byte count for it: test/staleackcheck.sh §(3) and §(4) exist to pin that a stale ack is visible as a ROW naming WHICH ack it is, and both of its fixtures are reports whose finding is gone — i.e. exactly the regressions="0" documents the fold would empty. A stale ack is most actionable on the clean report, because that is the run where you could clear it. Making the ledger's hygiene invisible precisely when it is cheap to fix is a worse trade than the bytes, so the rows stay and the lane report records the number instead. skilltruthcheck, skilldescbudgetcheck, skillscanreadcheck, readmedriftcheck, compactlegendcheck, legendcostcheck: PASS. Co-Authored-By: Claude Fable 5.1 --- CLAUDE.md | 6 ++++++ skills/ripwire-quality-bar/SKILL.md | 9 ++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 464ef16cf..eb28d6df5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -82,6 +82,7 @@ changed mid-compile; the discipline is the fix. ## Verify ```bash +./build/ripwire . --quality-delta --legend=compact # the "am I done" checkpoint — see the note below python3 test/pargates.py . ./build/ripwire -j 6 # the full gate suite, in parallel test/regression.sh # the same set, sequentially (authoritative list) LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./asan/ripwire >/dev/null @@ -89,6 +90,11 @@ LSAN_OPTIONS=suppressions=lsan_suppressions.txt ./asan/ripwire >/dev/null ./build/ripwire | xmllint --noout - # well-formedness gate ``` +`--legend=compact` on the checkpoint run is not cosmetic: on a clean report the legend is nearly the +whole document, and dropping it takes the run from 2,776 B to 454 B (measured 2026-09-10 on a +two-function fixture; 15,601 B to 8,775 B on this repo mid-change). The findings are byte-identical +either way — only the dictionary in front of them is shorter, and you already know it. + Run gates in the **foreground**. A new `test/*check.sh` must be listed in `test/regression.sh` in the same commit — `test/manifestcheck.sh` fails otherwise. diff --git a/skills/ripwire-quality-bar/SKILL.md b/skills/ripwire-quality-bar/SKILL.md index 9274c27a6..beacfeb53 100644 --- a/skills/ripwire-quality-bar/SKILL.md +++ b/skills/ripwire-quality-bar/SKILL.md @@ -119,8 +119,10 @@ of its own. Discount it accordingly: on a row whose other evidence is thin, `his something always fires.) ## The loop -1. **Zero-setup path:** just make your change, then run `ripwire --quality-delta` before you call it - done — in a git repo it auto-compares the working tree vs `git HEAD` (` --quality-delta --legend=compact` + before you call it done — add `--legend=compact` every time you run this in a loop: on a CLEAN report the + legend is nearly the whole payload (2,776 B to 454 B measured on a small fixture, 15,601 B to 8,775 B on + a mid-change repo), the rows are byte-identical either way, and you have already read the dictionary — in a git repo it auto-compares the working tree vs `git HEAD` (`` confirms it), no start-of-task action needed. **Tighter loop on a long change:** run `ripwire --quality-baseline` FIRST — **on a clean tree** — to pin an explicit floor (takes precedence over HEAD) so each edit deltas against the original start, not the last commit. On a tree that @@ -128,7 +130,8 @@ something always fires.) floor: commit first, or pass `--allow-dirty` to pin anyway, which stamps the absorbed count so every later report carries `baseline_absorbed="N"` and a green exit beside it reads "clean *since the pin*". 2. **Make your change.** -3. **Measure the delta** — `ripwire --quality-delta` → only the regressions you introduced, across the +3. **Measure the delta** — `ripwire --quality-delta --legend=compact` → only the regressions you + introduced, across the 10 kinds in the table below. Each emits `` (`members=` for duplication). Test-fixture dirs are exempt from `dead-code`; `short-horizon-churn` ignores your own current edit and exempts brand-new symbols/markdown/fixtures. Two exemptions are DISCLOSED on the report rather than From ca747de6f4bbba128f368bb597b79fd96244f7f1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:27:33 -0400 Subject: [PATCH 31/73] =?UTF-8?q?feat(mcp):=20no=5Froute=20=E2=80=94=20the?= =?UTF-8?q?=20CLI's=20own=20recovery=20from=20a=20route=20mis-fire,=20reac?= =?UTF-8?q?hable=20from=20an=20agent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `for`'s header carries route= (WHICH ranker answered, and why) and the tool's description tells the agent to read it. An agent that read it and disagreed had nowhere to go: the server refused `no_route` by name, and explore/pack_task had no such parameter either, so the CLI's answer to a route mis-fire was unreachable from MCP (2026-09-10 audit F-R1-07). The mis-fire is measured, not hypothetical: --for="parse tree" on this repo routes name-exact, returns three rows from bench/ and test/, and misses parseTree entirely, which --no-route finds at rank 1. One declared optional boolean on the verbs that ROUTE — for, explore, and explore's pack_task alias. It is the flag's TWIN, not a near-twin: the router is not asked, and exactly as verbs_for.h does under --no-route the query-shape demotion, the mention anchor and the co-change prior are skipped too, because each is part of the routed reading. There is then no route to disclose, so ctxRootOpen emits no route= — byte-for-byte what the CLI does. GATED AS A PARITY CLAIM, not as a feature. mcpforparitycheck gains seven arms, each asserted against the CLI's OWN behavior rather than a remembered rule: no route= under no_route (checked against the CLI's own --no-route output first, so a change there re-derives this arm instead of silently invalidating it); every CLI --no-route row present in the MCP no_route set; a quoted "true" refuses by name; pack_task honors it; a NON-routing verb (grep) still refuses it, because the declaration is per-verb. RED-FIRST: four of the seven fail against the pre-change binary. The first arm asserts the call ANSWERED — measured while writing the gate, two arms went GREEN against the pre-change binary purely on emptiness, since a refused call returns no content and an empty document trivially has no route=. MANIFEST COST, ATTRIBUTED: 41,220 -> 41,474 B, ceiling re-anchored 41,300 -> 41,650 (176 B headroom, the same posture as the three re-anchors above it in that file). Schemas 17,161 -> 17,415 (+254 = two property stanzas at +127, the envelope plus the description every declared property is obliged to carry). DESCRIPTIONS BYTE-IDENTICAL at 19,632 B: a first draft added a pointer clause to both tool descriptions and it was REMOVED rather than re-anchored around — that file's rule is that the ceiling moves for a declared argument's obliged bytes and never for prose. FIXED RATHER THAN ACKED: the verbosity row this change first raised on dispatchMcpLine (1,376 -> 1,387 lines) is gone — the second hand-rolled five-line boolean accumulate became ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, leaving the dispatcher SMALLER than before. The eight remaining gating rows are acked with their reasons: two defaulted-parameter contract changes (every pre-existing call site compiles unchanged), +4 complexity on each of the same two (the four !noRoute gates MIRROR verbs_for.h's four — collapsing them would be the MCP dialect deciding for itself what --no-route means), and four churn=self rows on the symbols this change edits. CLI untouched: default map, --for and --pack-task byte-identical to the pre-change binary. Gates green: mcpforparitycheck, mcpmanifestcheck, mcpcontractcheck, mcpstrictschemacheck, mcpverbscheck, mcpattrparitycheck, mcptranchecheck (docs/EVALS.md committed clean first), mcpclidiffcheck, mcpflagshipcheck, mcpeditpresencecheck, mcpframehonestycheck, docscommandscheck, printffmtparitycheck, manifestcheck, gatecountcheck, fixedbufsweep, nulbytecheck, testrowruncheck. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 14 +++++---- docs/EVALS.md | 36 +++++++++++++++++++++++ src/mcp.h | 19 +++++++++---- src/mcprefusal.h | 10 +++++-- src/mcpverbs.h | 33 +++++++++++++-------- test/mcpforparitycheck.sh | 60 +++++++++++++++++++++++++++++++++++++++ test/mcpmanifestcheck.sh | 16 ++++++++++- 7 files changed, 161 insertions(+), 27 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 9485cebd2..c2744f06e 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -12,7 +12,7 @@ ack api-surface 15754e3561a34f40 7 cid=e3721579f68947f6 deep-tail lane (docs/EVA ack api-surface 163c0a0eb3219fa9 5 cid=9e7d5dab8c14a887 R2: prEmptyRootTail gains the truncated= parameter it needs to carry budget-floor-exceeded — deliberate, 1 caller, incompatible=0 (--edit-check contract-change); prEmptyRootPrice is the new file-scope helper that decides the label and re-prices, keeping writePrContext's own complexity and LOC unchanged | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack api-surface 1689c98fa4eac33e 4 cid=f5ec9b69e2526e08 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 195e2b4deba2cee7 4 cid=c2581959226700a8 V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. -ack api-surface 1c11c9480374c3a4 4 cid=fc37866fa1022267 by=src/* M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. | prior: round-4 F-03 (MCP for vs CLI --for candidate-pool divergence): all three gating rows are this lane deliberate footprint. api-surface contract-change forTaskText 4->3 params is the FIX, not a cost: the removed parameter was int topK, and both call sites (the for dispatch arm in mcp.h and the batch sub-verb) fed it the SERVER-WIDE --top-k whose default is 200 — the ranked MAP row cap, which --for is documented to ignore (cli.h honorsTopK). So the MCP verb ranked a 5x wider candidate pool than its CLI twin on every call an agent could make (dropped_positive=169 vs 11 on parse arguments over this repo, and a different served symbol set), and the for tool schema exposes no cap that could reach the CLI behavior. A knob only ever fed the wrong value is not fixed by a better default; removing it is what makes the two dialects unable to drift again. Both call sites updated in the same commit; no external consumer exists (header-inline, MCP-internal). The two short-horizon-churn churn=self rows (forTaskText, runForLens) are the footprint of having edited functions this round already touched. The cap itself now lives ONCE as serialize.h kForLensDefaultTopN, read by the CLI lens and the MCP verb alike. Gate: test/mcpforparitycheck.sh, run RED first against a binary carrying only the new constant (arms 1/2/4 failed: MCP pool 200 vs CLI 40, 19 CLI-served rows absent from the MCP set, --top-k=5 vs 400 not byte-identical). droppedpositivecheck arm #6 pinned the old MCP head and is re-derived in the same commit with its reasoning. +ack api-surface 1c11c9480374c3a4 5 cid=27984fbee9fe12a9 by=src/* lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 1c873f03ef665f93 8 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 1da01868deceb731 7 cid=bce36ef4a75ab3fb by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack api-surface 1ed677c44b6c7ab6 5 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean @@ -120,6 +120,7 @@ ack api-surface db9af56c7a3f5bee 4 cid=bec957a0aa99147d P7 (terminality round A, ack api-surface dd2f935e1818171a 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack api-surface e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack api-surface e514a69013d0934c 6 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack api-surface e6bdc5187566f3d7 8 cid=615b35e39fe420e4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface ed05ee0357d016f1 4 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. @@ -335,7 +336,7 @@ ack complexity 19d43d944ddcd186 44 cid=533f2648b2fac227 timsort vendoring: every ack complexity 19e15f944de795a8 44 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack complexity 1aec4199f183ce56 326 cid=4dbaed74651e0046 capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity 1bded1f8f88d3b85 113 cid=cc39ef58a429fccb 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 complexity 1c11c9480374c3a4 51 cid=948595eb3858d81a F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). | prior: capture-audit 2026-09-04 wave-2 merge: L6 H14/M13 (confidence=/margin_pct=/at= on the MCP for root, budget_tokens=, the lens= declaration) + L10b finding 9 (route= trim) + the merge-fix that reconciled them (mention_anchored=/doc_mentions= served with the CLI's note wording, est_tokens= declared in lens=, and the CLI's confidence/at= sig-charge exemption applied so a disclosure never costs a ranked row — mcpforparitycheck (2) measured the row loss). Three conditional splices, each the CLI twin's exact rule; gates mcpattrparitycheck/mcpforparitycheck/budgetpolicycheck +ack complexity 1c11c9480374c3a4 59 cid=27984fbee9fe12a9 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). ack complexity 1cdbcf525f9e7871 60 cid=4a92ba4004e888e5 lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | 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 complexity 1da01868deceb731 21 cid=bce36ef4a75ab3fb by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack complexity 1e9c684ddd70ed1f 27 cid=f3502fc85213b238 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions @@ -430,6 +431,7 @@ ack complexity dda0db55532bd5e1 23 cid=848b85e47e6bd67c R1: +2 ccx on editplan:: ack complexity dda343d7d36edaba 77 cid=e8803c763bec269d L10: printLintRuleTallyRow's compiled= param and runLintRules' uncompiled-query mapping loop disambiguate a lint-rules query that failed to compile from one that legitimately found zero matches (see docs/PLAN lane L10, finding 3) — deliberate, backward-compatible (defaulted param, incompatible=0) ack complexity e14feca13c7ae680 57 cid=29dde7810cd9e120 M4 (lane ca-L2): writeHandoffPacket +24 LOC for three emitted facts (run= on , detached=1, candidates=/capped=) and the comments recording why the note match changed; complexity held at 56->57 minor by extracting verifiedNoteTargets() and kHandoffLegend | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity e1b4964cea59171b 18 cid=8ba981522099e145 H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. +ack complexity e514a69013d0934c 33 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B ack complexity e7f50422948f2c09 17 cid=6c7e771d42d81c16 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch ack complexity ec2848a4801493a2 51 cid=ed0825d14daf74d4 H11 (lane ca-L2): validateModifierGuards grows one more modifier arm (--allow-dirty, refused alone) in the flat if-chain this function IS; every sibling arm is the same 6 lines and splitting one out would hide it from the guard roster | prior: capture-audit 2026-09-04 lane L1 (flag-table universe), the seven gating rows of this lane's own five commits, each the documented cost of the guard/table it lands: parseArgs +4 cx is the --exclude= empty-value guard, one branch in the hand-written residue, the exact shape of its --and=/--not= siblings (M6); validateConfig +4 cx/+17 LOC is the bare --quality-ack pairing refusal plus the =REASON implication moved out of the parser arm (H10); validateModifierGuards +4 cx/+21 LOC is two pairing guards (--naming-locals refuse, --no-stable notice) in the helper every prior guard grew the same way (M16); kShapingVerbs 19->62 LOC is the ledger of every verb outside honorsPaging — 36 rows for 36 verbs, a table is its length, and test/shapingflagcheck.sh arm (F) is what stops it rotting (H3); main +11 LOC is the --json check moved ahead of the CLI edit bridge plus one call to refuseInertMainModifiers (H2/M16) — the two guards themselves and jsonUnsupportedVerb's allow-list walk live in their own helpers. Gates for all five written red first; battery green at this head. churn=self rows on jsonUnsupportedVerb/kViewFlags/writeAckRecords are this lane's own rewrites and are invisible in ref-pair mode by construction. ack complexity ed05ee0357d016f1 49 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean @@ -694,7 +696,7 @@ ack short-horizon-churn 1b4698af2f132a6c 8 cid=94f1d898d24191f6 arise-h2h lane 2 ack short-horizon-churn 1b4fa16bec10f38a 4 cid=a66263709c703a7d wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file ack short-horizon-churn 1bb815714e8daf12 13 cid=4966444e47d72201 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 1bded1f8f88d3b85 22 cid=3667b9c21e5f7382 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. | prior: arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked -ack short-horizon-churn 1c11c9480374c3a4 73 cid=948595eb3858d81a by=src/* F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites +ack short-horizon-churn 1c11c9480374c3a4 88 cid=27984fbee9fe12a9 by=src/* lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). ack short-horizon-churn 1cb330d8aa2e7320 3 cid=4b5bb633d5c8a487 abstention round-2 lane: short-horizon churn on the ARB harness is the lane's own second edit to bench/arb this wave — run_arb.py had to learn the three new facts and sweep() had to write them; the churn is the round, not new debt in the tool ack short-horizon-churn 1cdbcf525f9e7871 5 cid=4a92ba4004e888e5 lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack short-horizon-churn 1da01868deceb731 7 cid=add9cf9f36a5bdf8 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. @@ -868,7 +870,7 @@ ack short-horizon-churn 815fbf65eea5df47 5 cid=98131c6d45bba16a OPTREMARKS F3 (d ack short-horizon-churn 8192a44ad5eb2510 3 cid=ff366a47a1cdb76d by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 81efad81c80fc1cd 10 cid=99f4094a45b32fa2 F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack short-horizon-churn 824e30c136361009 4 cid=11f0e44f31665844 wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file -ack short-horizon-churn 82b1e3c6a4919914 15 cid=9a163fb0cde77116 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: 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 82b1e3c6a4919914 16 cid=d50eb6215046e886 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 82bf48718457b826 2 cid=36b75364752453aa L10: --doctor legend + blobs_floor= disambiguation (finding 6) — DoctorCacheStats gains capHit, doctorCacheStats sets it, runDoctor emits blobs_floor= and the new legend comment; short-horizon-churn and the verbosity bump are the direct, deliberate cost of that ack short-horizon-churn 83f27ab44a2fb8a7 2 cid=7b1641f001fe32cd P7 (terminality round A, lane R): droppedpositivecheck's verify_exact re-pinned to the FLAT --for --json sigs array (one row object per ranked symbol, no {p,symbols} wrapper) — this lane's own gate edit, not drift ack short-horizon-churn 8430c0a1b20d242e 13 cid=3edc3a9877090050 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. @@ -993,7 +995,7 @@ ack short-horizon-churn d3afca728392d688 6 cid=5765fc3aca7af273 OPTREMARKS F3 (d ack short-horizon-churn d42c85b67bd0956f 15 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) ack short-horizon-churn d44595768a7cf3af 2 cid=21539523996cfb13 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn d557a0077677ebd4 39 cid=7e6e8b041d54e368 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical -ack short-horizon-churn d63db6944aa504a7 34 cid=5d8c919443f11e2b answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: 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 d63db6944aa504a7 35 cid=7d756152fbee4105 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn d6e55b78e3a5fdda 7 cid=31161fe2ed1af5a5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d8aaf90801200a68 13 cid=d059a10443da1f80 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d9990dc7492a8bb2 20 cid=4646606f648e387e by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently @@ -1016,7 +1018,7 @@ ack short-horizon-churn e17ff0a2142942c4 4 PHP + Lua language port (lane/lang-ph ack short-horizon-churn e1f67e3817c9c097 7 cid=ff9ddf76360520be OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn e3c54d39d366fbaa 7 cid=402f938df07371e3 M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn e47e183ce409b3d1 5 cid=4a71f6160a322754 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: E3 (terminality round A, lane E): editpreviewcheck's fourth normalisation (strip the preview-only child), stated in the gate -ack short-horizon-churn e514a69013d0934c 59 cid=9f0bcb151f03e21f L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites +ack short-horizon-churn e514a69013d0934c 88 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites ack short-horizon-churn e602fc1bbc735406 5 cid=d2100a9e6dc3a52c OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn e6c86967176c9b8f 4 cid=bdc459a986a0a736 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn e6d92432fe4fd546 29 cid=b5e937b02b1a0718 by=src/* markdown/002-counter-saturate lane: the ONE gating row is short-horizon-churn on kParserVer itself, a constant whose entire contract is 'bump on any grammar/.scm/extraction change' — it churns once per grammar lane by design, and its in-window count (29) measures how often this repo changed a grammar, not thrash in this diff. This lane's owned-source footprint is exactly two constant lines (kParserVer 86->87 and quality.h's kIngestParserVerMirror mirroring it, gate-required to move together) plus four vendored C files outside the owned-source metrics. No function's complexity, LOC, nesting, params, duplication, dead-code, api-surface or error-masking moved: regressions=1 and that row IS the version bump. The extraction change it keys was MEASURED, not assumed: map output is byte-identical over 3 538 real files, but a constructed 256-column ATX line goes from n="BuriedHeading" emitted to absent, so a v86 blob can hold a phantom heading. Only the markdown half moves extraction: its indentation counters AND its fence level saturate, both being read by ordering tests against FIXED thresholds. The rust/lua/csharp casts reproduce upstream's value bit-for-bit and contribute nothing to this bump - measured, no extraction difference at any width. | prior: lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). diff --git a/docs/EVALS.md b/docs/EVALS.md index 9649faf4d..ac216d65a 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -2205,6 +2205,42 @@ checks into their own `flowTaskChoice` function (mirroring the existing `instrum extraction) and by inlining the small filler-word loop directly rather than introducing a shared helper that collided token-for-token with `weakSymbolCandidate`'s existing shape. +### MCP `no_route`: the CLI's recovery from a route mis-fire, reachable from an agent (2026-09-10) + +**The gap (audit F-R1-07).** `for`'s header carries `route=` — WHICH ranker answered and why — and the +tool's own description tells the agent to read it. An agent that read it and disagreed had nowhere to go: +`tools/call {"name":"for","arguments":{...,"no_route":true}}` was refused by name, and `explore` / +`pack_task` had no such parameter either. The CLI's own answer to a route mis-fire (`--no-route`) was +unreachable from the MCP surface. The mis-fire is measured, not hypothetical: `--for="parse tree"` on this +repo routes name-exact, returns three rows from `bench/` and `test/`, and misses `parseTree` entirely, +which `--no-route` finds at rank 1 (F-R1-06; the `ImplausibleAnchor` guard scales with corpus size, so +SMALL repos are the exposed ones). + +**What shipped.** One declared optional boolean, `no_route`, on `for` and `explore` (and its `pack_task` +alias). It is the CLI flag's twin, not a near-twin: under it the router is not asked, and — exactly as +`verbs_for.h` does under `--no-route` — the query-shape demotion, the mention anchor and the co-change +prior are all skipped, because each is part of the routed reading. There is then no route to disclose, so +`ctxRootOpen` emits no `route=`, byte-for-byte what the CLI does. + +**Gated as a parity claim, not as a feature.** `test/mcpforparitycheck.sh` gains seven arms, and each one +is asserted against the CLI's OWN behavior rather than against a remembered rule: the CLI emits no +`route=` under `--no-route`, so neither may the MCP twin; every CLI `--no-route` row must be present in +the MCP `no_route` set (subset, for the same payload reason the existing arms give); a quoted `"true"` is +a STRING and refuses by name; `pack_task` honors it; and a verb that does NOT route (`grep`) must still +refuse it, because the declaration is per-verb. The first arm asserts the call ANSWERED — measured while +writing the gate, two of the arms went GREEN against the pre-change binary purely on emptiness, since a +refused call returns no content and an empty document trivially has no `route=`. + +**Manifest cost, attributed.** 41,220 → 41,474 B; ceiling re-anchored 41,300 → 41,650. Schemas +17,161 → 17,415 (+254: two property stanzas at +127 each — the schema envelope plus the description every +declared property is obliged to carry). **Descriptions are byte-identical at 19,632 B**: a first draft +added a pointer clause to both tool descriptions and it was REMOVED rather than re-anchored around, +because that file's own rule is that the ceiling moves for a declared argument's obliged bytes and never +for prose. The `--quality-delta` verbosity row this change first raised on `dispatchMcpLine` +(1,376 → 1,387 lines) was likewise fixed rather than acked: the second hand-rolled five-line boolean +accumulate became ONE guarded `boolArg` reader that `post_check` now shares — the rule `intArg` already +states for the numeric fields — leaving the dispatcher smaller than before the change. + ### `--help-task` catalog tier: the verbs and the skills with no route (2026-09-10) **Two measurements, one cause.** `--help-task` recommended on **3 of 39** phrasings of the 13 surfaces diff --git a/src/mcp.h b/src/mcp.h index 218d60c6d..36036c7cd 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -860,12 +860,19 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // P9: the edit verbs' post-check opt-out. Default TRUE — the receipt carries its own // verification unless the caller says otherwise; a wrong-shaped value refuses like every other // typed argument rather than reading as absent (mcpBoolArg). - const McpBoolArg postCheckArg = mcpBoolArg( args, "post_check" ); - if( shapeRefusal.empty() && !postCheckArg.refusal.empty() ) + // ONE guarded reader per TYPE, the rule `intArg` above states for the numeric fields: a second + // boolean argument (no_route, 2026-09-10) would otherwise be a second five-line hand-rolled + // accumulate, which is how two spellings of one gate come to disagree. + const auto boolArg = [ & ]( const char* field ) -> McpBoolArg { - shapeRefusal = postCheckArg.refusal; - } + const McpBoolArg a = mcpBoolArg( args, field ); + if( shapeRefusal.empty() && !a.refusal.empty() ) { shapeRefusal = a.refusal; } + return a; + }; + const McpBoolArg postCheckArg = boolArg( "post_check" ); const bool postCheck = !postCheckArg.isPresent || postCheckArg.value; + // F-R1-07: the CLI --no-route over MCP, on the verbs that ROUTE (for / explore / pack_task). + const bool noRoute = boolArg( "no_route" ).value; const std::string text = strArg( "text" ); // insert_before/after const std::string handle = strArg( "handle" ); // T4 fetch_body const std::string kind = strArg( "kind" ); // exemplar kind token; whereis/stray_content/flags/doc_drift name filter @@ -1471,7 +1478,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo { // M13: `budget_tokens` — the same knob the CLI --for takes, absent here until now. const std::string t = forTaskText( path, task, redactPtr, - budgetArg.isPresent ? std::size_t( budgetArg.value ) : 0 ); + budgetArg.isPresent ? std::size_t( budgetArg.value ) : 0, noRoute ); resp = t.empty() ? errResult( -32602, "no symbols found" ) : textResult( t ); } else if( name == "lego" && !path.empty() && !type.empty() ) @@ -1677,7 +1684,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo static_assert( kMcpRecallTopKMax == 1000, "the top_k refusal names the band 1..1000 in mcprefusal.h's kMcpValueFields and in the " "tools/list memory_recall stanza — move all three together" ); - resp = textResult( packTaskText( path, task, budgetTokens, redactPtr, partitionCount ) ); + resp = textResult( packTaskText( path, task, budgetTokens, redactPtr, partitionCount, noRoute ) ); } // L4: `from_trace` — maps a pasted stack-trace/sanitizer/compiler-error TEXT onto indexed symbols // (fromTraceBundleText, tracelocus.h) — the SAME assembler --from-trace's CLI path calls. diff --git a/src/mcprefusal.h b/src/mcprefusal.h index d1ba9c55f..56013b387 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -334,6 +334,12 @@ inline constexpr McpValueSpec kMcpValueFields[] = { // argument's obliged description and never for prose, and prose there would cost ~680 B against 159 B // of headroom. The refusal example names the NON-default value, which is the one a caller has to type. { "legend", "a STRING legend posture: compact (the default) or full", "legend=\"full\"" }, + // ── boolean ── + // F-R1-07 (2026-09-10 audit): the CLI's own answer to a route MIS-FIRE is to re-run with --no-route, + // and the MCP surface had no equivalent — an agent that reads route= and disagrees was told WHICH ranker + // answered and given no way to ask for the other one. Advertised as a real boolean, not a string, so a + // quoted "true" refuses instead of being guessed at (mcpBoolArg). + { "no_route", "a BOOLEAN: true forces plain subtoken+body BM25 (omit it for the default router)", "no_route=true", "boolean" }, // ── the ENVELOPE, outside `params` (§B6 M6/M7) ── // These four were read through the bare findString/findObject path, which collapses "absent" onto // "present but not the shape I read" — so `"method":5` became `-32700 "parse error"` (a JSON that parsed @@ -982,7 +988,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "memory_recall", "path task top_k budget_tokens" }, { "situational_awareness", "path diff files" }, { "mentions", "path paths symbol limit offset" }, - { "for", "path paths task budget_tokens" }, + { "for", "path paths task budget_tokens no_route" }, { "lego", "path paths type legend" }, { "owners", "path symbol limit offset legend" }, { "fetch_body", "path handle start_line end_line" }, @@ -998,7 +1004,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "uses", "path paths symbol limit offset legend" }, { "path_between", "path paths from to legend" }, { "connect", "path paths symbols radius legend" }, - { "explore", "path paths task budget_tokens partition legend" }, + { "explore", "path paths task budget_tokens partition legend no_route" }, { "from_trace", "path paths trace budget_tokens legend" }, // 2026-09-10: limit/offset are DECLARED here because the verb now HONORS them (mcpPageArgs -> the // unflagged-row window in editcheck.h), the same rule the `impact`/`uses` rows above state. They diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 59517cebc..2eb41c480 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1503,7 +1503,7 @@ inline void priceForTaskRoot( std::string& doc, std::size_t budgetTokens ) } inline std::string forTaskText( const std::string& root, const std::string& task, RedactCounts* redact = nullptr, - std::size_t budgetTokens = 0 ) + std::size_t budgetTokens = 0, bool noRoute = false ) { const std::size_t forBudgetBytes = budgetTokens > 0 ? budgetBytesForTokens( budgetTokens ) : kForPayloadBudgetBytes; @@ -1513,7 +1513,13 @@ inline std::string forTaskText( const std::string& root, const std::string& task // query-shape classifier picks name-exact vs subtoken+body BM25, so an identifier query lands the // symbol (recall@1 ~99% vs ~77% plain) while conceptual queries keep the subtoken+body behavior // (lexical.h chooseForRanker). MCP-only agents get the same optimization the CLI ships. - const RouteChoice rc = chooseForRanker( ing, task ); + // `noRoute` is the MCP twin of the CLI --no-route (2026-09-10 audit F-R1-07): the router is not asked, + // so the ranker is the plain subtoken+body default, and — exactly as verbs_for.h does under the flag — + // the query-shape demotion, the mention anchor and the co-change prior are all skipped, because each of + // them is part of the routed reading. A default-constructed RouteChoice IS that reading: SubtokenBody, + // no reason, no anchors, so there is no route= to disclose and ctxRootOpen omits the attribute, which is + // byte-for-byte what the CLI emits under --no-route. + const RouteChoice rc = noRoute ? RouteChoice{} : chooseForRanker( ing, task ); // NOT const: LB-A's relevance floor narrows it below, once every boost has landed on lensRank. The // MaxScore pruning bound two stanzas down consumes the PRE-floor value, which is the safe direction — // a bound computed for a larger K can only keep more candidates, never fewer. @@ -1532,7 +1538,7 @@ inline std::string forTaskText( const std::string& root, const std::string& task // mention anchor) as the CLI --for. This dialect always routes, so the shape is always asked for and // the disclosure always has a route= to ride in. const queryshape::Verdict shape = queryshape::classify( task ); - const std::vector tierMul = rankTierSymbolMultipliersShaped( ing, shape.fires() ); + const std::vector tierMul = rankTierSymbolMultipliersShaped( ing, !noRoute && shape.fires() ); // deep-tail: this bundle now serves the file-grain tail, a full-distribution consumer — the H2 // MaxScore prune bound is 0 (exhaustive) here for the same reason the CLI --for passes // fullDistribution (a pruned tail would make total= mode-dependent and its order incomplete). @@ -1550,7 +1556,7 @@ inline std::string forTaskText( const std::string& root, const std::string& task // The CLI twin's lr.capAttrs: the INDEXING caps that cut this ranking, same names, same order, so the // two surfaces cannot disagree about what was dropped (mention.h CapDisclosure). "" unless one bit. std::string capAttrs; - if( !std::getenv( "RIPWIRE_NO_MENTION" ) ) + if( !noRoute && !std::getenv( "RIPWIRE_NO_MENTION" ) ) { MentionBoostInfo mentionInfo; if( applyMentionBoost( ing, task, lensRank, &mentionInfo ) ) @@ -1574,7 +1580,7 @@ inline std::string forTaskText( const std::string& root, const std::string& task // has no per-call flags — RIPWIRE_COCHANGE=1 (the shared opt-in env) enables it here. // Inert without usable history (depth-1 / non-git ⇒ support threshold unreachable ⇒ byte-identical output). std::string boostNote; - if( std::getenv( "RIPWIRE_COCHANGE" ) && hasEnclosingGitRepo( root ) ) + if( !noRoute && std::getenv( "RIPWIRE_COCHANGE" ) && hasEnclosingGitRepo( root ) ) { CommitWindowCensus coCensus; // the kCoBoostMaxFilesPerCommit census (gitmine.h) const auto coSets = gitRecentCommitFileSets( root, ing, kCoBoostCommitWindow, kCoBoostMaxFilesPerCommit, UINT32_MAX, &coCensus ); @@ -1678,7 +1684,8 @@ inline std::string forTaskText( const std::string& root, const std::string& task // §L10b + verify-wave2 F6: same trim as the CLI --for twin (verbs_for.h) — no leading " [" and no // trailing "]"; the value lands only in route=, where the attribute quote is the delimiter. const std::string mcpForAtAttrStr = gitstamp::atAttr( root ); // M10's at=, computed once: spliced onto the root AND exempted from the sigs charge below - std::string rootOpenStr = ctxRootOpen( task, "routed: " + rc.reason + shapeDemotionNote( shape ), flRootArg ); // §B1.7: same root attrs as the CLI twin + std::string rootOpenStr = ctxRootOpen( task, noRoute ? std::string() : ( "routed: " + rc.reason + shapeDemotionNote( shape ) ), + flRootArg ); // §B1.7: same root attrs as the CLI twin (no route= under no_route, as --no-route) if( !rootOpenStr.empty() && rootOpenStr.back() == '>' ) { // Attribute ORDER matches the CLI twin's: confidence/margin_pct, then at=, then this dialect's own @@ -3369,14 +3376,16 @@ inline std::string qualityBaselineJson( const std::string& root, std::string& er // value outside 2..16, which is silently clamped OFF rather than erroring an otherwise valid explore call) // ⇒ the plain single-bundle form, byte-identical to before. inline std::string packTaskText( const std::string& root, const std::string& task, std::size_t budgetTokens, - RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0 ) + RedactCounts* redact = nullptr, std::uint32_t partitionCount = 0, bool noRoute = false ) { const McpIndex& ix = getIndex( root ); const IngestResult& ing = ix.ing; const Graph& g = ix.g; LensRanking lr; - const RouteChoice rc = chooseForRanker( ing, task ); + // See forTaskText's own note: `noRoute` is the CLI --no-route over MCP, and it skips the shape demotion, + // the mention anchor and the co-change prior with the ranker, because all four are the routed reading. + const RouteChoice rc = noRoute ? RouteChoice{} : chooseForRanker( ing, task ); std::vector ifaceExact( ing.symbols.size(), 0 ); for( std::size_t i = 0; i < ix.g.implementors.size() && i < ifaceExact.size(); ++i ) { @@ -3388,13 +3397,13 @@ inline std::string packTaskText( const std::string& root, const std::string& tas // Query SHAPE + §P4 tier de-prioritization — same classifier, same multiplier, same order (before the // mention anchor) as CLI --pack-task. const queryshape::Verdict shape = queryshape::classify( task ); - const std::vector tierMul = rankTierSymbolMultipliersShaped( ing, shape.fires() ); + const std::vector tierMul = rankTierSymbolMultipliersShaped( ing, !noRoute && shape.fires() ); lr.rank = ( rc.which == LexMode::NameExact ) ? lexicalScoresNameExactRanked( ing, task, &tierMul ) : lexicalScoresTiered( ing, g.outOff, g.outTargets, task, 0, &ifaceExact, &tierMul ); // §L10b + verify-wave2 F6: same trim as the other route= construction sites — neither bracket. - lr.routeNote = "routed: " + rc.reason + shapeDemotionNote( shape ); + lr.routeNote = noRoute ? std::string() : ( "routed: " + rc.reason + shapeDemotionNote( shape ) ); - if( !std::getenv( "RIPWIRE_NO_MENTION" ) ) + if( !noRoute && !std::getenv( "RIPWIRE_NO_MENTION" ) ) { MentionBoostInfo mentionInfo; if( applyMentionBoost( ing, task, lr.rank, &mentionInfo ) ) @@ -3406,7 +3415,7 @@ inline std::string packTaskText( const std::string& root, const std::string& tas } absorbCapDisclosure( mentionInfo.caps, lr.mentionNote, lr.capAttrs, lr.capJson ); } - if( std::getenv( "RIPWIRE_COCHANGE" ) && hasEnclosingGitRepo( root ) ) + if( !noRoute && std::getenv( "RIPWIRE_COCHANGE" ) && hasEnclosingGitRepo( root ) ) { CommitWindowCensus coCensus; const auto coSets = gitRecentCommitFileSets( root, ing, kCoBoostCommitWindow, kCoBoostMaxFilesPerCommit, UINT32_MAX, &coCensus ); diff --git a/test/mcpforparitycheck.sh b/test/mcpforparitycheck.sh index dabf01f80..225d8ac6b 100755 --- a/test/mcpforparitycheck.sh +++ b/test/mcpforparitycheck.sh @@ -218,6 +218,66 @@ for tb in 900 1200 1600 2000 6000; do fi done +# ── (6) no_route: the CLI's own recovery from a route MIS-FIRE, reachable from MCP (audit F-R1-07) ──────── +# `for`'s header names WHICH ranker answered and why. Until 2026-09-10 an agent that read route= and +# disagreed had no way to ask for the other one: the server refused `no_route` by name, so the CLI's own +# answer to a mis-fire was unreachable from the MCP surface. These arms are RED against a pre-change binary +# (the first returns -32602 "unknown field: 'no_route'"). +mcp_for_nr(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"%s","no_route":%s}}}\n' \ + "$CORPUS" "$1" "$2" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py"; } +NR_Q="parse tree" +mcp_for "$NR_Q" >"$TMP/nr.routed.xml" +mcp_for_nr "$NR_Q" true >"$TMP/nr.off.xml" +cli_for "$NR_Q" >"$TMP/nr.cli.routed.xml" +cli_for "$NR_Q" --no-route >"$TMP/nr.cli.off.xml" +grep -q 'route="' "$TMP/nr.routed.xml" \ + && ok "(6) MCP for still discloses route= by default" \ + || no "(6) MCP for lost its route= disclosure" +# FIRST, that the call ANSWERED. A refused call returns no content, and an empty document trivially +# satisfies "no route=" and "not the routed row set" — the two arms below would then pass against a binary +# that does not know the argument at all. Measured while writing this gate: against the pre-change binary +# those two arms went green on emptiness, which is exactly the false-green this arm exists to prevent. +if ! grep -q '"$TMP/nr.off.rows" +python3 "$TMP/rows.py" <"$TMP/nr.cli.off.xml" >"$TMP/nr.cli.off.rows" +python3 "$TMP/rows.py" <"$TMP/nr.routed.xml" >"$TMP/nr.routed.rows" +if [ ! -s "$TMP/nr.off.rows" ] || [ ! -s "$TMP/nr.cli.off.rows" ]; then + no "(6) one of the two no-route dialects served no rows — this arm measured nothing" +else + cmp -s "$TMP/nr.off.rows" "$TMP/nr.routed.rows" \ + && no "(6) no_route:true served the SAME rows as the routed call — the argument is inert" \ + || ok "(6) no_route:true changes the served set, as --no-route does on the CLI" + missing="$( comm -23 "$TMP/nr.cli.off.rows" "$TMP/nr.off.rows" | head -3 )" + [ -z "$missing" ] \ + && ok "(6) every CLI --no-route row is present in the MCP no_route set" \ + || no "(6) MCP no_route dropped CLI --no-route rows: $( printf '%s' "$missing" | tr '\n' ' ' )" +fi +# Typed, like every other MCP argument: a quoted "true" is a STRING and refuses rather than being guessed. +QT="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"for","arguments":{"path":"%s","task":"x","no_route":"true"}}}\n' "$CORPUS" | "$BIN" --mcp 2>/dev/null )" +case "$QT" in *'invalid value for field: no_route'*) ok "(6) a quoted \"true\" refuses by name, never read as absent";; *) no "(6) no_route:\"true\" did not refuse: $( printf '%s' "$QT" | head -c 160 )";; esac +# explore (and its pack_task alias) route too, and declare the same argument. +EX="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"pack_task","arguments":{"path":"%s","task":"%s","no_route":true}}}\n' "$CORPUS" "$NR_Q" | "$BIN" --mcp 2>/dev/null | python3 "$TMP/mcptext.py" )" +{ [ -n "$EX" ] && ! printf '%s' "$EX" | grep -q 'route="'; } \ + && ok "(6) explore/pack_task honors no_route too (bundle served, no route=)" \ + || no "(6) explore/pack_task did not honor no_route" +# A verb that does NOT route must still refuse the argument — the declaration is per-verb, not global. +GR="$( printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"grep","arguments":{"path":"%s","pattern":"x","no_route":true}}}\n' "$CORPUS" | "$BIN" --mcp 2>/dev/null )" +case "$GR" in *"unknown field: 'no_route'"*) ok "(6) a non-routing verb still refuses no_route by name";; *) no "(6) grep accepted no_route: $( printf '%s' "$GR" | head -c 160 )";; esac + # ── determinism + well-formedness on the MCP dialect ───────────────────────────────────────────────────── mcp_for "$INERT_Q" >"$TMP/d1.xml"; mcp_for "$INERT_Q" >"$TMP/d2.xml" cmp -s "$TMP/d1.xml" "$TMP/d2.xml" \ diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 39034d0f2..fc8bf0439 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -111,6 +111,20 @@ tools = json.loads( line )[ "result" ][ "tools" ] # undefined. Same rule as the two re-anchors above (a DECLARED argument, its bytes attributed here, in the # commit that lands it, never prose) and the same posture: 171 B of headroom, less than one more argument. # +# RE-ANCHORED 2026-09-10 (MCP no_route, audit F-R1-07): 41,300 → 41,650, measured 41,474 (from 41,220). +# ONE declared optional argument, `no_route`, on the TWO verbs that ROUTE — `for` and `explore` (and its +# `pack_task` alias, which shares explore's stanza) — the MCP twin of the CLI --no-route. Attributed against +# a build of the parent commit: schemas 17,161 → 17,415 B (+254, two property stanzas at +127 each: the +# schema envelope plus the description every declared property is obliged to carry) and DESCRIPTIONS +# BYTE-IDENTICAL at 19,632 B. A first draft added a pointer clause to both tool descriptions (+43 B after +# trimming to one); it was removed rather than re-anchored around, because this file's rule is that the +# ceiling moves for a declared argument's obliged bytes and never for prose, and the schema property is +# where a client renders an argument anyway. Same posture as the three re-anchors above: 176 B of headroom. +# What it buys: `for`'s header names WHICH ranker answered and why, and until now an agent that read route= +# and disagreed had no way to ask for the other one — the CLI's own recovery from a route mis-fire was +# unreachable from MCP (measured: --for="parse tree" on this repo routes name-exact and returns three rows +# from bench/ and test/, missing parseTree, which --no-route finds at rank 1). +# # ── THE CEILING, DECIDED 2026-09-05 (terminality round A, lane M / M2): IT STAYS 41,000. ───────────── # Registered as an OWNER DECISION with the arithmetic, so it can be overruled with numbers rather than # re-litigated. Measured on this tree at the M1 commit: manifest 40,841 B (~10,210 tokens), descriptions @@ -160,7 +174,7 @@ tools = json.loads( line )[ "result" ][ "tools" ] # TOTAL 40,986 -> 40,902 B; nothing else moved. Raw wire bytes (this gate measures json.dumps # with ensure_ascii, which spends 6 for each em dash instead of 3): 40,901 -> 40,811. # Headroom goes back UP, 14 B -> 98 B. That is item 5 below working, not a new allowance. -CEILING = 41300 +CEILING = 41650 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) From 45065b6b961e656f04ae5667f97dd3ff97ec4df1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:29:04 -0400 Subject: [PATCH 32/73] fix(gate): the escaper harness generator was not sanitizer-clean MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G1 compiles the gates with -fsanitize=integer -fno-sanitize-recover=all, and a plain wrapping `state * 6364136223846793005ull` in the harness's own RNG aborts the run on unsigned-integer-overflow before a single escaper is compared: emitescape_harness.cpp:223:23: runtime error: unsigned integer overflow: 11400714819323198485 * 6364136223846793005 cannot be represented in 'unsigned long long' The gate script itself does not pass the sanitizer flags, so this was invisible from the gate and would have surfaced only as a G1 leg failing on a harness nobody had run instrumented. Route both multiplies through hashutil::multiplyModulo64 (128-bit widen and mask) — the exact reason test/harnesscommon.h's generator already does, quoted in its own comment. Same fixed seed, same 222,682 inputs, same MUT count (168,423 differ). Verified: the harness now compiles and runs clean under the full guardrail set (-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow -fno-sanitize-recover=all), 4 checks ALL PASS, and ./asan/ripwire is clean over the every-byte fixture on --top-k XML/JSON, --for and --expand, and over ripwire's own tree XML/JSON. --- test/emitescape_harness.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/test/emitescape_harness.cpp b/test/emitescape_harness.cpp index b9427a216..c17fe8a2d 100644 --- a/test/emitescape_harness.cpp +++ b/test/emitescape_harness.cpp @@ -29,6 +29,7 @@ // Exit 0 = all pass; nonzero = a failure. #include "../src/serialize.h" +#include "../src/infra/hashutil.h" #include "../src/infra/jsonesc.h" #include @@ -213,16 +214,21 @@ static std::string escapeXmlMutatedSet( std::string_view s ) // ── the corpus ──────────────────────────────────────────────────────────────────────────────────────── -// UB-free deterministic generator (same shape as test/harnesscommon.h's, kept local so this TU needs -// no extra include path). +// Deterministic generator, sanitizer-clean by construction: the multiplies go through +// hashutil::multiplyModulo64 (a 128-bit widen and mask) rather than wrapping in 64 bits, because G1 +// compiles these gates with -fsanitize=integer -fno-sanitize-recover=all and a plain `state * k` aborts +// the run on unsigned-integer-overflow. Same reasoning, same shape as test/harnesscommon.h's generator; +// kept local so this TU needs no second include path. Fixed seed => a failure reproduces anywhere. struct Rng { std::uint64_t state = 0x9E3779B97F4A7C15ull; std::uint64_t next() noexcept { - state = state * 6364136223846793005ull ^ 1442695040888963407ull; + state = rw::hashutil::multiplyModulo64( state, 6364136223846793005ull ) ^ 1442695040888963407ull; std::uint64_t m = state; - m ^= m >> 33; m *= 0xFF51AFD7ED558CCDull; m ^= m >> 33; + m ^= m >> 33; + m = rw::hashutil::multiplyModulo64( m, 0xFF51AFD7ED558CCDull ); + m ^= m >> 33; return m; } }; From cca0bf8d358dc3713e71062bf24ca200f92965b1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:29:51 -0400 Subject: [PATCH 33/73] quality(self): clear the five findings this branch's own dials reported on it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--quality-delta=05f4b892..HEAD` with the branch's own binary, which is the only honest way to close a round that changes what that verb says. Five gating findings, all real, all fixed rather than acked: complexity src/lintrules.h::rw::errorMaskBlockIsEmpty 4 -> 25, a bar CROSSING — the widened swallow test grew a whole comment scanner inside a predicate. The comment-only half is errorMaskBlockIsCommentOnly now and the caller is two lines. duplication + new-clone-of-reused-helper, registeredMacroNames | vendoredPathPrefixes, 114 tokens — and the reuse kind is exactly right: vendoredPathPrefixes was written longhand as "built-ins, then config, then sort+unique", which is character for character what registeredMacroNames already did. Both call mergeBuiltinsWithConfig now. The third row (editplan::callersUnionSize sharing the same shape) goes with it. verbosity src/quality.h::quality::readRegisterMacrosConfig 58 -> 69, a bar CROSSING — the second config key pushed the line loop over. The value-token loop is appendConfigValueTokens. This is the loop the round is about, run on the round: the growth tier let the two CROSSINGS through while demoting computeDelta's +59 ccx (243 -> 302, +24%) to minor, the clone dial did not swallow a genuine 114-token copy of a reused helper, and the report that named all five fit in 8,775 bytes with --legend=compact. Co-Authored-By: Claude Fable 5.1 --- src/lintrules.h | 41 +++++++++---------- src/quality.h | 105 ++++++++++++++++++++++++++---------------------- 2 files changed, 76 insertions(+), 70 deletions(-) diff --git a/src/lintrules.h b/src/lintrules.h index a84223c9d..3af6182a6 100644 --- a/src/lintrules.h +++ b/src/lintrules.h @@ -1078,20 +1078,11 @@ inline constexpr std::array kErrorMaskRules = { { // keeps the block out. Both directions of the imprecision lose recall rather than manufacturing a finding. // The @p capture filter in findErrorMasking depends on a bare identifier ("catch"/"then") answering false // here, and it still does: no braces, no match. -inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept +// The comment-only half, factored out so neither this test nor its caller crosses a complexity bar: is +// `collapsed` a brace pair whose entire interior is one comment? Called only after the exact-`{}` test has +// already failed. +inline bool errorMaskBlockIsCommentOnly( std::string_view collapsed ) noexcept { - std::string stripped; - for( char c : collapsed ) - { - if( c != ' ' && c != '\t' && c != '\n' && c != '\r' ) - { - stripped.push_back( c ); - } - } - if( stripped == "{}" ) - { - return true; - } std::string_view t = collapsed; while( !t.empty() && ( t.front() == ' ' || t.front() == '\t' ) ) { t.remove_prefix( 1 ); } while( !t.empty() && ( t.back() == ' ' || t.back() == '\t' ) ) { t.remove_suffix( 1 ); } @@ -1104,26 +1095,30 @@ inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept { return false; // a statement survives inside it — not a swallow } - const std::size_t slash = mid.find( "//" ); - const std::size_t block = mid.find( "/*" ); - const std::size_t hash = mid.find( '#' ); - std::size_t first = std::string_view::npos; - for( std::size_t c : { slash, block, hash } ) + std::size_t first = std::string_view::npos; + for( std::string_view opener : { std::string_view( "//" ), std::string_view( "/*" ), std::string_view( "#" ) } ) { - if( c != std::string_view::npos && ( first == std::string_view::npos || c < first ) ) { first = c; } + const std::size_t at = mid.find( opener ); + if( at != std::string_view::npos && ( first == std::string_view::npos || at < first ) ) { first = at; } } if( first == std::string_view::npos ) { return false; // content that is not a comment at all } - for( std::size_t i = 0; i < first; ++i ) + return mid.substr( 0, first ).find_first_not_of( " \t" ) == std::string_view::npos; +} + +inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept +{ + std::string stripped; + for( char c : collapsed ) { - if( mid[i] != ' ' && mid[i] != '\t' ) + if( c != ' ' && c != '\t' && c != '\n' && c != '\r' ) { - return false; // something precedes the comment + stripped.push_back( c ); } } - return true; + return stripped == "{}" || errorMaskBlockIsCommentOnly( collapsed ); } // One error-masking hit: the suppressing block's file + start byte (so a caller can attribute it to the diff --git a/src/quality.h b/src/quality.h index fff0c0cf4..1c06700e3 100644 --- a/src/quality.h +++ b/src/quality.h @@ -57,6 +57,7 @@ #include #include // std::is_trivially_copyable_v — the qsnap POD put/get static_assert #include +#include // mergeBuiltinsWithConfig — a non-owning view over either built-in list #include namespace rw @@ -355,6 +356,39 @@ struct RegisterMacrosConfig // accepted. Absent/unreadable/empty file yields two empty lists — INERTNESS CONTRACT: no config file // changes nothing about this run's set of exempted names (kBuiltinRegisterMacros still applies), and an // unrecognized key is disclosed, never a refusal — a typo in an otherwise-inert config must not fail a run. +// One directive's VALUE list: comma-separated tokens, each trimmed, each admitted by its key's own rule. +// Hoisted out of the line loop so that loop stays readable (and under its bars) now that the file carries two +// keys. A PATH is root-relative with no '..' segment and no leading '/'; anything else is a value this file's +// grammar defines nothing for and is dropped rather than guessed at, the same posture the macro-token check +// takes. Never throws, never warns: a malformed VALUE is inert, and only a malformed KEY is disclosed. +inline void appendConfigValueTokens( std::string_view rest, bool isVendor, RegisterMacrosConfig& out ) +{ + std::size_t start = 0; + while( start <= rest.size() ) + { + const std::size_t comma = rest.find( ',', start ); + std::string_view tok( rest.data() + start, ( comma == std::string_view::npos ? rest.size() : comma ) - start ); + while( !tok.empty() && ( tok.back() == ' ' || tok.back() == '\t' ) ) { tok.remove_suffix( 1 ); } + while( !tok.empty() && ( tok.front() == ' ' || tok.front() == '\t' ) ) { tok.remove_prefix( 1 ); } + if( isVendor ) + { + if( !tok.empty() && tok.front() != '/' && tok.find( ".." ) == std::string_view::npos ) + { + out.vendoredPaths.emplace_back( tok ); + } + } + else if( isValidMacroToken( tok ) ) + { + out.names.emplace_back( tok ); + } + if( comma == std::string_view::npos ) + { + break; + } + start = comma + 1; + } +} + inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) { RegisterMacrosConfig out; @@ -390,34 +424,7 @@ inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) out.unrecognizedKeys.emplace_back( key ); // F-13: disclosed, not skipped continue; } - std::string_view rest = line.substr( eq + 1 ); - std::size_t start = 0; - while( start <= rest.size() ) - { - const std::size_t comma = rest.find( ',', start ); - std::string_view tok( rest.data() + start, ( comma == std::string_view::npos ? rest.size() : comma ) - start ); - while( !tok.empty() && ( tok.back() == ' ' || tok.back() == '\t' ) ) { tok.remove_suffix( 1 ); } - while( !tok.empty() && ( tok.front() == ' ' || tok.front() == '\t' ) ) { tok.remove_prefix( 1 ); } - if( isVendor ) - { - // A PATH, not an identifier: root-relative, no '..' segment, no leading '/' — anything else is - // a value this file's grammar defines nothing for and is dropped rather than guessed at, the - // same posture the macro-token check takes. - if( !tok.empty() && tok.front() != '/' && tok.find( ".." ) == std::string_view::npos ) - { - out.vendoredPaths.emplace_back( tok ); - } - } - else if( isValidMacroToken( tok ) ) - { - out.names.emplace_back( tok ); - } - if( comma == std::string_view::npos ) - { - break; - } - start = comma + 1; - } + appendConfigValueTokens( line.substr( eq + 1 ), isVendor, out ); } std::sort( out.names.begin(), out.names.end() ); out.names.erase( std::unique( out.names.begin(), out.names.end() ), out.names.end() ); @@ -430,16 +437,31 @@ inline RegisterMacrosConfig readRegisterMacrosConfig( std::string_view root ) // The combined, sorted, deduped registered-macro name list for ONE run: the built-ins above plus whatever // .ripwire_config's register_macros= adds. Sorted so nothing downstream needs its own re-sort. -inline std::vector registeredMacroNames( std::string_view root ) +// The ONE shape both .ripwire_config consumers need: this tool's built-in list, plus whatever the repo's own +// config adds, sorted and deduped so nothing downstream re-sorts. Factored the moment the second consumer +// existed — `--quality-delta` reported vendoredPathPrefixes as a 114-token clone of this function the first +// time it was written out longhand, which is the kind's whole job. +inline std::vector mergeBuiltinsWithConfig( std::span builtins, + std::vector fromConfig ) { - std::vector names( kBuiltinRegisterMacros.begin(), kBuiltinRegisterMacros.end() ); - for( std::string& extra : readRegisterMacrosConfig( root ).names ) + std::vector out; + out.reserve( builtins.size() + fromConfig.size() ); + for( std::string_view b : builtins ) { - names.push_back( std::move( extra ) ); + out.emplace_back( b ); + } + for( std::string& extra : fromConfig ) + { + out.push_back( std::move( extra ) ); } - std::sort( names.begin(), names.end() ); - names.erase( std::unique( names.begin(), names.end() ), names.end() ); - return names; + std::sort( out.begin(), out.end() ); + out.erase( std::unique( out.begin(), out.end() ), out.end() ); + return out; +} + +inline std::vector registeredMacroNames( std::string_view root ) +{ + return mergeBuiltinsWithConfig( kBuiltinRegisterMacros, readRegisterMacrosConfig( root ).names ); } // Q-DIAL-5 (2026-09-10) — VENDORED PATHS: code this repo CARRIES but did not WRITE. No such notion existed @@ -457,18 +479,7 @@ inline constexpr std::array kBuiltinVendoredPrefixes = { "t inline std::vector vendoredPathPrefixes( std::string_view root ) { - std::vector out; - for( std::string_view p : kBuiltinVendoredPrefixes ) - { - out.emplace_back( p ); - } - for( std::string& extra : readRegisterMacrosConfig( root ).vendoredPaths ) - { - out.push_back( std::move( extra ) ); - } - std::sort( out.begin(), out.end() ); - out.erase( std::unique( out.begin(), out.end() ), out.end() ); - return out; + return mergeBuiltinsWithConfig( kBuiltinVendoredPrefixes, readRegisterMacrosConfig( root ).vendoredPaths ); } // `rel` is ROOT-RELATIVE (the relForHash spelling every sidecar key uses). A prefix ending in '/' names a From 932ce5c89654aa85e132d21b1983c6ef31514773 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:35:43 -0400 Subject: [PATCH 34/73] =?UTF-8?q?docs(gate):=20the=20residual=20child-iter?= =?UTF-8?q?ator=20cost,=20attributed=20=E2=80=94=20bindsVisitNode=20is=203?= =?UTF-8?q?0%=20of=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records what a `sample` of the FIXED binary on a cold llvm-project map says about the walks this lane did NOT convert, so the next lane starts from a number instead of a judgment call. `ts_node_child_iterator_next` is still the #1 busy leaf at 14.26% (127 453 busy samples, a 12 s window of a 46 s run at load 36 — a window share, not a whole-run share). Attributing each of its samples to the nearest non-tree-sitter caller: captureTagsFacts 7 304, bindsVisitNode 5 614, qualifierOf 2 629, enclosingScopeOf 1 040, cc_isCountableLocalDecl 676, cc_walk 531. bindsVisitNode is the one class-2 site this lane deliberately left indexed (it needs the INDEX for `ts_node_field_name_for_child`), and it is 30% of that leaf's cost — so the semantic change its fix needs, the cursor's O(1) `ts_tree_cursor_current_field_name`, has a measured reason to be worth its own gate. Comment-only; no arm, no count, no binary change. Co-Authored-By: Claude Fable 5.1 --- test/childwalkscalecheck.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh index 6f740202c..56ec5be9d 100755 --- a/test/childwalkscalecheck.sh +++ b/test/childwalkscalecheck.sh @@ -72,7 +72,12 @@ # `ts_node_field_name_for_child( n, i )`, which is itself index-based, so collecting the children # would leave the loop quadratic in the field lookup. The cursor's own O(1) # `ts_tree_cursor_current_field_name` is the real fix and is a SEMANTIC change (alias/extra handling) -# that needs its own gate — not folded into a no-output-change lane. +# that needs its own gate — not folded into a no-output-change lane. It is worth that gate: on a cold +# llvm-project map of the FIXED binary (`sample`, 12 s of a 46 s run, 127 453 busy leaf samples), +# `ts_node_child_iterator_next` is still the #1 leaf at 14.26%, and attributing each of its samples to +# the nearest non-tree-sitter caller puts bindsVisitNode SECOND at 5 614 samples — 30% of that leaf's +# whole cost, behind captureTagsFacts' 7 304 (the tags-query pass, a different shape). Then +# qualifierOf 2 629, enclosingScopeOf 1 040, cc_isCountableLocalDecl 676, cc_walk 531. # * src/ingest_names.h:61 (firstChildOfType) keeps the indexed form: both callers pass a # `using_declaration` / `qualified_identifier`, whose width comes from the grammar, and a per-call # cursor allocation would cost more than the scan it replaces. Class 3 in practice, not class 2. From b41c2264492b1de81cf96afb1b2fafca5089463d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:36:48 -0400 Subject: [PATCH 35/73] chore(pins): regenerate docs/LIMITS.md and re-pin test/qschemetrip.hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both are build products of this branch's own source, not hand-written numbers. docs/LIMITS.md — python3 docs/limits_build.py, for the two thresholds the growth tier added (kMaterialGrowthPct, kSubBarGrowthPct). limitstablecheck was red on the stale file. test/qschemetrip.hash — UPDATE_GOLDEN=1 test/qschemetripcheck.sh. The manifest hashes the SOURCE TEXT of every function a cached Snapshot's meaning depends on, and this branch moved three of them (isDeadCandidate's predicate, computeSnapshot's loc record, and the .ripwire_config reader). The gate's own instructions name the order: bump the scheme first, THEN re-pin — kQSnapCacheScheme went 8 -> 9 -> 10 across the dead-code and verbosity dials, each with its reason recorded at the constant, so the re-pin follows two real bumps rather than papering over an unbumped change. kParserVer and its mirrors did NOT move: no extraction changed, only what the delta MEANS by the numbers it already had. Co-Authored-By: Claude Fable 5.1 --- docs/LIMITS.md | 2 +- test/qschemetrip.hash | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 237a4eeb5..7c8c85da2 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -295,7 +295,7 @@ Discloses: `coboost_commits_capped`, `hits_capped`, `unindexed_candidates_capped | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kBatchCap` | `16` | 4218 | — | max sub-queries processed per batch; excess is REPORTED, never silently dropped | +| `kBatchCap` | `16` | 4221 | — | max sub-queries processed per batch; excess is REPORTED, never silently dropped | | `kMcpPageValueMax` | `1000000000` | 306 | — | == cli.h's kPageValueMax | | `kMcpRecallTopKMax` | `1000` | 312 | — | — | diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 555a2498a..0591eabcd 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -0f5848def5a25b232149e534dc72bdcc04ad6a8dd12af90191e4a5b8112568e6 +6d1c4def21a509de1a656c7b291114b66ba1bc899985cbdfefd046c2127f23c2 From 8e1a04c90fdd3fdca2b23a91f56cb82d53987cb1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:45:50 -0400 Subject: [PATCH 36/73] =?UTF-8?q?quality(clones):=20withdraw=20the=20one-f?= =?UTF-8?q?ile=20drop=20=E2=80=94=20two=20of=20this=20repo's=20own=20gates?= =?UTF-8?q?=20say=20it=20is=20wrong?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dial round dropped a clone group whose members all live in one file, on the audit's labelling rule W3b: "sibling/alternate implementations" (mergeHi|mergeLo, gallopLeft|gallopRight), 13 groups labelled WRONG by it. Landing it turned two gates red, and reading them settles the question against the rule: test/clonededupcheck.sh's ENTIRE positive case is `calcDup` appended to the same file as the reused helper `calc` it copies — the canonical new-clone-of-reused-helper. test/qualitycheck.sh §3 pins a `dup1`/`dup2` pair inside one new file as a duplication finding, with `callc()` calling each exactly once so neither has fan-in. A fan-in guard rescued the first (a helper three call sites use is eroded wherever the copy lands) but not the second, and the second is not wrong either: a copy-pasted body is duplication whether it lands next door or across the tree, and file identity cannot tell a deliberate specialization from a paste. Two gates written deliberately outrank a hand rule in a labelling script, so the clause is withdrawn rather than argued around, and the reason is written where the next person will meet it. The overload-set drop (a) and the vendored drop (c) stay: `emitTo|emitTo` is the language, and upstream's shape is not ours. Both are group properties that cannot be mistaken for a paste. WORKING-TREE REPLAY, 12 landed commits, final tiers: | | before | with one-file | withdrawn | | rows | 266 | 208 | 209 | | duplication rows | 9 | 8 | 9 | | gating rows | 171 | 32 | 32 | | commits that gate | 12/12 | 8/12 | 8/12 | | gating precision TRUE | 2% | 12% | 12% | | WRONG rows (gating) | 1 | 0 | 0 | One row, no gating change, no precision change on this population — the clause was costing two gates for a single non-gating row. test/qddialscheck.sh §5's arm is inverted with the reason beside it, so the withdrawal is pinned rather than merely removed: a same-file copy must STILL be reported. qualitycheck, qualitysignalcheck (whose api-surface new-symbol arms follow the count, not the retired row), clonededupcheck, qddialscheck: PASS. Co-Authored-By: Claude Fable 5.1 --- src/quality.h | 37 +++++++++++++++++-------------------- test/qddialscheck.sh | 11 +++++++---- test/qualitycheck.sh | 6 ++++-- test/qualitysignalcheck.sh | 13 ++++++++++--- 4 files changed, 38 insertions(+), 29 deletions(-) diff --git a/src/quality.h b/src/quality.h index 1c06700e3..1abed843e 100644 --- a/src/quality.h +++ b/src/quality.h @@ -6098,16 +6098,22 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap // (a) ONE OVERLOAD SET — every member shares one canonical id. Overloads of a function are near- // identical by construction (emitTo|emitTo, sort::stable|sort::stable); reporting them as a copy is // reporting the language. - // (b) ONE FILE AND NO REUSED MEMBER — every member lives in the same file and none of them is a helper - // the tree already leans on (fan-in >= kReusedHelperMinFanin). A sibling pair inside one body of - // code is an alternate implementation the author is looking at while writing it (mergeHi|mergeLo, - // gallopLeft|gallopRight), not the reuse decline these kinds exist to catch. The fan-in half is not - // a hedge: copying a helper that three call sites already use is a real erosion whether the copy - // lands next door or across the tree, and dropping it on file identity alone silently retired - // test/clonededupcheck.sh's whole positive case — which is how this clause was found. + // (b) WITHDRAWN — see the note below. // (c) VENDORED — every member sits under a vendored path (see isVendoredPath). Upstream's shape is not // this repo's to fix, and one commit produced 9 such rows. - // NOT a token floor: raising kMinCloneTokens was measured and REFUTED. The canonical true positive + // ONE-FILE IS NOT ON THIS LIST, and the reason is worth more than the rows it would have dropped. The + // audit's labelling rule W3b called a group whose members share one file "sibling/alternate + // implementations" (mergeHi|mergeLo, gallopLeft|gallopRight) and 13 groups were labelled WRONG by it. The + // clause was written, and TWO of this repo's own gates went red on it: test/clonededupcheck.sh's whole + // positive case is a copy of a reused helper appended to the SAME file, and test/qualitycheck.sh §3 pins + // a dup1/dup2 pair inside one new file as a duplication finding. Both were written deliberately, and both + // are right: a copy-pasted body is duplication wherever it lands, and file identity cannot tell a + // deliberate specialization from a paste. A hand rule in a labelling script does not outrank two gates + // that encode the opposite policy, so the drop is withdrawn rather than argued around. The rows it aimed + // at need the discriminator the acks themselves use — no shared domain identifier — which is a + // cloneidiom.h round, not a group-shape predicate. + // + // NOT a token floor either: raising kMinCloneTokens was measured and REFUTED. The canonical true positive // (synthetic S1, a 12-line copy of a reused helper) is 59 tokens, while the idiom collisions in the same // replay run 22, 24, 31, 36, 56, 65, 66, 74, 78, 91, 92, 96, 114 and 127 — a floor above 22 loses true // positives before it clears any noise. Token count is the wrong axis. @@ -6118,13 +6124,9 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap { return false; } - const auto* ro = g.inEdges.rowOffsets(); bool oneId = true; - bool oneFile = true; bool allVend = true; - bool reused = false; std::string_view firstId; - std::uint32_t firstFile = 0; bool haveFirst = false; for( NodeId m : cg.members ) { @@ -6141,19 +6143,14 @@ inline std::vector computeDelta( const IngestResult& ing, const Grap { allVend = false; } - if( std::uint32_t( ro[m + 1] - ro[m] ) >= kReusedHelperMinFanin ) - { - reused = true; - } if( !haveFirst ) { - firstId = g.canonId[m]; firstFile = f; haveFirst = true; + firstId = g.canonId[m]; haveFirst = true; continue; } - if( g.canonId[m] != firstId ) { oneId = false; } - if( f != firstFile ) { oneFile = false; } + if( g.canonId[m] != firstId ) { oneId = false; } } - return oneId || ( oneFile && !reused ) || allVend; + return oneId || allVend; }; gtl::btree_map dupSeen; diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 4df8798a6..0fbf5b2fc 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -14,8 +14,8 @@ # gate on a bar CROSSING or >= 25% growth, and a sub-bar doubling is a minor row rather than silence. # 4. api-surface — new-symbol rows are a header COUNT, a surface that SHRANK is not a regression, and a # single trailing DEFAULTED parameter is minor. -# 5. duplication / new-clone-of-reused-helper — an overload set, a one-file group and a vendored path are -# not this change's duplication. +# 5. duplication / new-clone-of-reused-helper — an overload set and a vendored path are not this change's +# duplication; a same-file copy IS, which is why the audit's one-file drop was withdrawn. # 6. error-masking — a block whose only content is a COMMENT is a swallow. # # Fixtures are built in temp dirs (git-init where a section needs history); the repo is never touched. @@ -292,9 +292,12 @@ ODP="$( cd "$DP" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" dup(){ rows "$ODP" | grep 'kind="duplication"' | grep "$1"; } dup 'alpha' >/dev/null && ok "duplication: the CROSS-FILE copy is still reported (synthetic S1's shape)" \ || { no "duplication: the cross-file copy was dropped — the dial cut a true positive"; rows "$ODP" | grep duplication; } +# THE ONE-FILE DROP WAS WITHDRAWN, and this arm is what it was withdrawn in favour of: a copy-pasted body is +# duplication wherever it lands. test/clonededupcheck.sh and test/qualitycheck.sh §3 both pin exactly this +# shape, deliberately, and a hand rule in the audit's labelling script does not outrank two gates. dup 'sameA' >/dev/null \ - && { no "duplication: a group confined to ONE FILE is still reported"; rows "$ODP" | grep duplication; } \ - || ok "duplication: a group confined to one file produces no row" + && ok "duplication: a same-file copy is STILL reported (the one-file drop was withdrawn)" \ + || { no "duplication: the same-file copy was dropped — clonededupcheck and qualitycheck pin this shape"; rows "$ODP" | grep duplication; } # NON-VACUITY, checked rather than assumed: three of the four built-in prefixes (third_party/, vendor/, # node_modules/) are already dropped by the CRAWLER, so a fixture placed there would pass this arm on any # binary ever built — the first draft of it did. external/ is the one the crawler indexes, so it is the one diff --git a/test/qualitycheck.sh b/test/qualitycheck.sh index ffc69a32d..b94e4770b 100755 --- a/test/qualitycheck.sh +++ b/test/qualitycheck.sh @@ -97,8 +97,10 @@ printf '%s' "$OD" | grep -q 'kind="nesting" sym="deepen" was="' \ && ok "nesting regression: deepen flagged (nesting grew over the bar)" || no "nesting regression missing" printf '%s' "$OD" | grep -q 'kind="params" sym="widen" was="' \ && ok "params regression: widen flagged (param count grew over the bar)" || no "params regression missing" -printf '%s' "$OD" | grep -q 'kind="api-surface" sym="newly_public"' \ - && ok "api-surface regression: newly_public flagged (new exported symbol)" || no "api-surface regression missing" +# Q-DIAL-4 (2026-09-10): a brand-new export is counted on the root, not printed as a row it can never gate on. +printf '%s' "$OD" | grep -q 'api-new-surface="[1-9]' \ + && ok "api-surface: the new exported symbol is counted on the root (api-new-surface)" \ + || { no "api-surface: newly_public not counted"; printf '%s\n' "$OD" | tr '>' '\n' | grep -E ' printf '#pragma once\nint pubfn( int a );\nint pubfn( int a ){ return a; }\nint newpubfn(){ return 1; }\n' > "$API/include/api.h" ONS="$( cd "$API" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" -printf '%s' "$ONS" | grep -q 'kind="api-surface" sym="newpubfn"[^/]*sev="minor"[^/]*surface="new-symbol"' \ - && ok "api-surface tiering: brand-new public symbol → sev=\"minor\" surface=\"new-symbol\"" \ - || { no "api-surface tiering: new-symbol case not tiered correctly"; printf '%s\n' "$ONS" | tr '>' '\n' | grep '' '\n' | grep -E '' '\n' | grep 'kind="api-surface"' | grep -q 'newpubfn' \ + && { no "api-surface tiering: the new-symbol row is still emitted beside the count"; printf '%s\n' "$ONS" | tr '>' '\n' | grep '/dev/null 2>&1; echo $? )" [ "$ENS" = 0 ] && ok "api-surface tiering: new-symbol-only run does not gate exit 2" || no "api-surface tiering: new-symbol run should exit 0 (got $ENS)" From 67b930d69be243dc5b1005dbfc4813be42fb24b0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 19:56:35 -0400 Subject: [PATCH 37/73] perf(strkern): the byte-set scan's tail stops calling the oracle it was never meant to ship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `strkern::findByteset` ended EVERY call — SIMD block taken or not — in `findByteset_scalar`, and that function was the harness ORACLE: it re-derived a four-word bitmap from the 32-byte set on entry, in a deliberately different representation so a bug in the (b >> 3, b & 7) packing could not hide behind a reference that shared it. Right for the gate it was written for; fatal for a hot path. Lane M measured the consequence and refused the kernel over it: routing rw::escapeXml through findByteset took escapeXml from 4.62% to 22.46% of a warm --top-k=100000 map, made the whole map 6-18% slower, and left the scan 3.5x-7x SLOWER than the per-byte switch it replaced on 6..40-byte inputs. Nothing in strkerncheck could see it, because the defect was performance, not correctness. The fix is that the SET carries its derived representations, not the scan: Byteset256 { bits[32], words[4] } both written by add(), at construction, constexpr — every set this repo ships is a constexpr initialiser, so both are built at COMPILE time and neither costs a shipped instruction. findByteset_scalar the SHIPPED scalar twin and the tail every vector path falls into: one O(1) bit test per byte against set.words. No preamble, nothing derived per call. findByteset_oracle the re-derivation, unchanged in spirit and now GATE-ONLY: it rebuilds the four words from `bits` through contains(), so it still checks the packing, and it is reachable from nothing in src/. WHY THE TAIL IS THE WHOLE STORY. Instrumented escapeXml on a warm `--top-k=100000` map (a scratch RIPWIRE_ESCAPE_TRACE build, reverted) and read back the real inputs: corpus calls median len < 16 B >= 64 B special-byte density ripwire 44,670 11 63.9% 4.2% 0.89% go 340,814 8 81.9% 0.1% 0.00% django 148,681 17 44.2% 17.9% 0.00% 44-82% of calls never reach a 16-byte NEON block at all. A byte-set scan's hot caller is ALL tail, which is exactly why an O(256) preamble in the tail cost 5x and why the SIMD loop above it was irrelevant to the number M measured. GATE, in the same commit (test/strkern_harness.cpp): * E0 — Byteset256 carries two agreeing representations: contains() vs containsWord() for all 5 sets x 256 byte values, and the stored words re-derived from bits, word by word. * D1/E1 and G3 now compare vector == scalar == ORACLE == naive, so the tail and the oracle cannot drift apart silently — the thing that would have caught this defect had the oracle been beside the tail instead of underneath it. * CAN GO RED: -DSTRKERN_MUTATE=1 gains one non-SIMD mutation (add() drops the high half of the set from `words` only). Before: 8 arms red. After: 9. `strkerncheck` runs 14 arms green on NEON and 14 green on the x86_64/AVX2 mirror under Rosetta 2. Byte-identical, 15/15: 3 corpora (a frozen git-archive of this tree, go, django) x 5 verbs (--top-k=100000, --for, --grep, --pack-task, --lint) — stdout, stderr and exit code all cmp-equal against the pre-change binary. Nothing shipped calls findByteset yet; that is the next commit. Box load 24-37 throughout (uptime beside every measurement in the lane report). --- src/infra/strkern.h | 75 ++++++++++++++++++++++++++++++++++++---- test/strkern_harness.cpp | 53 ++++++++++++++++++++++++---- 2 files changed, 115 insertions(+), 13 deletions(-) diff --git a/src/infra/strkern.h b/src/infra/strkern.h index e3ade8539..8eab06adf 100644 --- a/src/infra/strkern.h +++ b/src/infra/strkern.h @@ -574,14 +574,49 @@ inline std::size_t find3( const char* p, std::size_t n, const char* needle ) noe return tail == n - k ? n : k + tail; } -// A 256-bit byte set, laid out the way `sz_find_byteset` wants it: bit ( b & 7 ) of byte ( b >> 3 ). That -// decomposition is what makes the SIMD test two table lookups — the (b >> 3) lookup fetches the set's row -// byte, the (b & 7) lookup fetches the bit to test it with. +// A 256-bit byte set that carries BOTH of the representations its users need, DERIVED ONCE at +// construction — usually at compile time, since every set this repo ships is `constexpr`: +// +// bits[ 32 ] bit ( b & 7 ) of byte ( b >> 3 ). What the SIMD paths want (`sz_find_byteset`'s layout): +// that decomposition makes the membership test two table lookups — the (b >> 3) lookup +// fetches the set's row byte, the (b & 7) lookup fetches the bit to test it with. +// words[ 4 ] bit ( b & 63 ) of word ( b >> 6 ). What a SCALAR loop wants: one indexed load, one +// variable shift, one AND per byte — and, decisively, NO per-call preamble. +// +// WHY THE SET CARRIES ITS OWN WORDS RATHER THAN A SCAN DERIVING THEM. The tail of a block scan is short +// by construction (< kBlockBytes), and the hot callers of a byte-set scan are ALL tail: an escaper over a +// 6..40-byte symbol name or path never enters a 16- or 32-byte block loop at all. A tail that re-derives +// its set representation on entry therefore does O( 256 ) work before it looks at one byte of input. +// +// That is measured, not feared. Until 2026-09-10 `findByteset` ended every call in an oracle that +// re-derived these four words from `bits` on entry; routing rw::escapeXml through it took escapeXml from +// 4.62% to 22.46% of a warm `--top-k=100000` map, made the whole map 6-18% slower, and left the scan +// 3.5x-7x SLOWER than the per-byte switch it replaced on 6..40-byte inputs (lane M's report, the row that +// refused it). Deriving in `add` instead costs one extra OR per inserted byte, at construction. +// +// The oracle is still here, still a different derivation — it just is not the shipped tail any more. See +// findByteset_oracle below. struct Byteset256 { - std::uint8_t bits[ 32 ] = {}; + std::uint8_t bits[ 32 ] = {}; // ( b >> 3, b & 7 ) — the SIMD tables' layout + std::uint64_t words[ 4 ] = {}; // ( b >> 6, b & 63 ) — the scalar tail's O( 1 ) test - constexpr void add( unsigned char b ) noexcept { bits[ b >> 3 ] |= std::uint8_t( 1u << ( b & 7u ) ); } + constexpr void add( unsigned char b ) noexcept + { + bits[ b >> 3 ] |= std::uint8_t( 1u << ( b & 7u ) ); +#if defined( STRKERN_MUTATE ) + // MUTATION (gate's can-go-red arm): the high half of the set never reaches `words`, so the scalar + // tail and the oracle disagree above 0x7F. This is the ONE mutation that is not SIMD-only, and it + // exists because the defect it models is not SIMD-only either: a second stored derivation of the + // same set can go stale silently, and E0/D1-E1 are the arms that must see it. Never define this. + if( b < 0x80u ) + { + words[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); + } +#else + words[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); +#endif + } constexpr void addRange( unsigned char lo, unsigned char hi ) noexcept { for( unsigned b = lo; b <= unsigned( hi ); ++b ) @@ -590,11 +625,35 @@ struct Byteset256 } } constexpr bool contains( unsigned char b ) const noexcept { return ( bits[ b >> 3 ] >> ( b & 7u ) ) & 1u; } + // The same question asked of the OTHER member. A caller never needs this — `contains` is the answer — + // but the gate does: the two representations are built by the same `add`, so an arm that reads both + // back is what proves they cannot drift (test/verify_strkern.cpp, "Byteset256 carries two agreeing + // representations"). + constexpr bool containsWord( unsigned char b ) const noexcept { return ( words[ b >> 6 ] >> ( b & 63u ) ) & 1u; } }; -// The scalar oracle deliberately uses a DIFFERENT representation of the same set — four u64 words, tested -// with a shift — so a bug in the (b >> 3, b & 7) packing cannot hide behind an oracle that shares it. +// THE SHIPPED SCALAR TWIN, and the tail every vector path below falls into. One O( 1 ) bit test per byte +// against the set's own precomputed words; no preamble, nothing derived per call. This is the function a +// target with neither NEON nor AVX2 runs, and it is also the function a 6-byte input runs on every target. inline std::size_t findByteset_scalar( const char* p, std::size_t n, const Byteset256& set ) noexcept +{ + for( std::size_t k = 0; k < n; ++k ) + { + const unsigned char c = static_cast( p[ k ] ); + if( ( set.words[ c >> 6 ] >> ( c & 63u ) ) & 1u ) + { + return k; + } + } + return n; +} + +// THE ORACLE — for the gate, and for nothing else. It answers the same question by re-deriving the four +// words from `bits` through `contains`, i.e. through the (b >> 3, b & 7) packing, so a bug in EITHER +// representation cannot hide behind a reference that shares it. That derivation is a 256-iteration loop +// per call: it is why this must never be reachable from a shipped path, and the header comment above is +// the record of what happened when it was. Not called from anywhere in src/. +inline std::size_t findByteset_oracle( const char* p, std::size_t n, const Byteset256& set ) noexcept { std::uint64_t words[ 4 ] = { 0, 0, 0, 0 }; for( unsigned b = 0; b < 256u; ++b ) @@ -670,6 +729,8 @@ inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& } } #endif + // The tail is the scalar twin above — one bit test per byte against the set's OWN words. It is NOT + // the oracle: see the Byteset256 note for the 4.62% -> 22.46% that rule is written from. const std::size_t tail = findByteset_scalar( p + k, n - k, set ); return tail == n - k ? n : k + tail; } diff --git a/test/strkern_harness.cpp b/test/strkern_harness.cpp index d2ab779e2..4940ccf24 100644 --- a/test/strkern_harness.cpp +++ b/test/strkern_harness.cpp @@ -444,6 +444,43 @@ int main( int argc, char** argv ) const sk::Byteset256* kSets[] = { &setEmpty, &setFull, &setOne, &setHighOnly, &setXml }; const char* kSetNames[] = { "empty", "full", "one", "high", "xml" }; + // E0 — Byteset256 now stores TWO derivations of the same set (bits for the SIMD table lookups, words + // for the scalar tail's O(1) test), both written by add(). Nothing else in the header would notice one + // of them going stale, so this arm reads both back for every set and every byte value. + std::string repFail; + for( std::size_t si = 0; si < 5 && repFail.empty(); ++si ) + { + std::uint64_t rederived[ 4 ] = { 0, 0, 0, 0 }; + for( unsigned b = 0; b < 256u; ++b ) + { + const unsigned char c = static_cast< unsigned char >( b ); + if( kSets[ si ]->contains( c ) ) + { + rederived[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); + } + if( kSets[ si ]->contains( c ) != kSets[ si ]->containsWord( c ) ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "set=%s byte=%02x bits=%d words=%d", kSetNames[ si ], b, + int( kSets[ si ]->contains( c ) ), int( kSets[ si ]->containsWord( c ) ) ); + repFail = msg; + } + } + for( int w = 0; w < 4 && repFail.empty(); ++w ) + { + if( rederived[ w ] != kSets[ si ]->words[ w ] ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "set=%s word[%d] stored=%016llx rederived=%016llx", + kSetNames[ si ], w, ( unsigned long long )kSets[ si ]->words[ w ], + ( unsigned long long )rederived[ w ] ); + repFail = msg; + } + } + } + checkf( repFail.empty(), "E0 Byteset256 carries two agreeing representations (bits vs words, 5 sets x 256 bytes)%s%s", + repFail.empty() ? "" : " — ", repFail.c_str() ); + for( int iter = 0; iter < 100000; ++iter ) { const Alphabet alpha = Alphabet( iter & 3 ); @@ -564,16 +601,19 @@ int main( int argc, char** argv ) const std::size_t si = std::size_t( gen.next() % 5u ); const std::size_t gotS = sk::findByteset( buf.data(), n, *kSets[ si ] ); const std::size_t refS = sk::findByteset_scalar( buf.data(), n, *kSets[ si ] ); + // the ORACLE re-derives the four words from `bits` through contains(), so this third value is + // what stops the set's two stored representations from drifting apart unseen (2026-09-10). + const std::size_t oraS = sk::findByteset_oracle( buf.data(), n, *kSets[ si ] ); std::size_t naiS = n; for( std::size_t k = 0; k < n; ++k ) { if( kSets[ si ]->contains( static_cast< unsigned char >( buf[ k ] ) ) ) { naiS = k; break; } } - if( findFail.empty() && ( gotS != refS || gotS != naiS ) ) + if( findFail.empty() && ( gotS != refS || gotS != oraS || gotS != naiS ) ) { - char msg[ 192 ]; - std::snprintf( msg, sizeof( msg ), "findByteset[%s] iter=%d got=%zu ref=%zu naive=%zu n=%zu", - kSetNames[ si ], iter, gotS, refS, naiS, n ); + char msg[ 224 ]; + std::snprintf( msg, sizeof( msg ), "findByteset[%s] iter=%d got=%zu ref=%zu oracle=%zu naive=%zu n=%zu", + kSetNames[ si ], iter, gotS, refS, oraS, naiS, n ); findFail = msg; } } @@ -597,7 +637,7 @@ int main( int argc, char** argv ) bufferCount, foldFail.empty() ? "" : " — ", foldFail.c_str() ); checkf( eqFail.empty(), "C1 lowerFoldedEquals vector == scalar, equal and perturbed, %zu buffers%s%s", bufferCount, eqFail.empty() ? "" : " — ", eqFail.c_str() ); - checkf( findFail.empty(), "D1/E1 findByte / find3 / findByteset vector == scalar == naive oracle, %zu buffers%s%s", + checkf( findFail.empty(), "D1/E1 findByte / find3 / findByteset vector == scalar == oracle == naive, %zu buffers%s%s", bufferCount, findFail.empty() ? "" : " — ", findFail.c_str() ); checkf( tokFail.empty(), "F2 tokenizer == pre-change walker (spans + fused hashes) on %zu random buffers%s%s", bufferCount, tokFail.empty() ? "" : " — ", tokFail.c_str() ); @@ -661,7 +701,8 @@ int main( int argc, char** argv ) } const std::size_t gotS = sk::findByteset( text.data(), text.size(), setXml ); const std::size_t refS = sk::findByteset_scalar( text.data(), text.size(), setXml ); - if( got3 != ref3 || got3 != nai3 || gotS != refS ) + const std::size_t oraS = sk::findByteset_oracle( text.data(), text.size(), setXml ); + if( got3 != ref3 || got3 != nai3 || gotS != refS || gotS != oraS ) { realFindFail = names[ fi ]; } From f0186fe811d0fe9a18a8e5ca0f1d2642437e4cff Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 20:01:07 -0400 Subject: [PATCH 38/73] =?UTF-8?q?refactor(strkern):=20one=20header=20?= =?UTF-8?q?=E2=80=94=20the=20sibling=20scan=20folds=20back=20in=20and=20th?= =?UTF-8?q?e=20escapers=20get=20the=20block=20loop?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's rule (2026-09-10) is that ALL SIMD string kernels live in src/infra/strkern.h. Lane M could not follow it: `findByteset`'s tail was the oracle, so the escapers could not call the one header without paying 5x, and M shipped `src/infra/strkern_find.h` instead — recording in that file's own closing note that "if strkern.h grows a shipped tail beside its oracle, this file collapses into a call to it and the SIMD path comes along for free". The previous commit grew the tail. This commit collapses the file. * `appendCleanRun` moves into strkern.h as section 4, unchanged in body except for the scan it calls, with M's attributions and reasoning kept verbatim. * `findBytesetRun` does not move: with the tail fixed it WAS `findByteset_scalar`, character for character. One fewer name for one function. * src/infra/strkern_find.h deleted; src/serialize.h and src/infra/jsonesc.h include strkern.h. * `rg -n 'arm_neon.h|immintrin.h' src/` now lists strkern.h plus exactly the four pre-existing infra headers (dynamic_map.hpp, radixSort.h, sparseCsr.h, fixedStr.h). No sixth home. THE SCAN UNDER appendCleanRun IS NOW `findByteset` — block loop and all — because it measured better, on the inputs ripwire actually escapes rather than on a synthetic band. A scratch RIPWIRE_ESCAPE_TRACE build (reverted, never committed) captured every escapeXml argument of a warm `--top-k=100000` map; the traces were replayed end to end through escapeXml, best of 9, four independent process runs, box load 24-33. Milliseconds for the whole trace: corpus calls median len per-byte switch + scalar scan + findByteset ripwire 44,670 11 1.60-2.00 0.81-1.01 0.71-0.96 -15% go 340,814 8 6.85-7.62 4.57-4.85 4.74-5.08 +1% django 148,681 17 8.00-9.12 3.80-4.30 2.97-3.11 -23% Two win, one ties inside the noise band, none loses — so the block loop ships and no caller has to choose a scan. go is the tie because its median escaped string is 8 bytes and 82% of its calls never reach a 16-byte block; that is the same fact that made the old tail so expensive. WHAT IS NOT CLAIMED. escapeXml is ~1% of a warm map on these corpora (0.82-1.18% by `sample`), so a 15-23% cut in it is ~0.2% of the verb. The interleaved whole-verb A/B was run anyway (12 arms a side per corpus, user+sys) and resolves nothing: ripwire tree 0.110 / 0.110 s median, django 0.540 / 0.530, go 0.940 / 0.940. The `sample` arm M's report prescribes was also run and is REPORTED AS INCONCLUSIVE rather than dressed up: at this box load the escaper draws 27-58 samples out of 3.3k-7.1k busy per arm, which cannot resolve a 20% change in a 1% site. The trace replay is the instrument; the sampler is not, here. Byte-identical, 21/21: 3 corpora (frozen git-archive of this tree, go, django) x 7 surfaces (--top-k=100000 XML and JSON, --for XML and JSON, --grep, --pack-task, --lint) — stdout, stderr and exit code all cmp-equal against the pre-change binary. Determinism (two runs cmp-equal) and `xmllint --noout` clean. emitescapecheck ALL PASS (222,682 inputs, MUT arm sees 168,423 disagree); strkerncheck PASS (14 arms NEON, 14 arms AVX2 under Rosetta 2, mutation reds 9). --- src/infra/jsonesc.h | 2 +- src/infra/strkern.h | 59 +++++++++++++++++++++++++++ src/infra/strkern_find.h | 86 ---------------------------------------- src/serialize.h | 2 +- 4 files changed, 61 insertions(+), 88 deletions(-) delete mode 100644 src/infra/strkern_find.h diff --git a/src/infra/jsonesc.h b/src/infra/jsonesc.h index 95b0993d8..24d017696 100644 --- a/src/infra/jsonesc.h +++ b/src/infra/jsonesc.h @@ -36,7 +36,7 @@ // this header is a pure internal refactor: verified byte-identical against the pre-unification // implementations. -#include "strkern_find.h" // S5: findBytesetRun — the run-copy skip that replaces escapeInto's per-byte switch. +#include "strkern.h" // S5: appendCleanRun — the run-copy skip that replaces escapeInto's per-byte switch. // Still zero includes ABOVE src/infra (strkern.h itself pulls only // // / plus the ISA intrinsic header), so the no-cycle property this // header was factored out for is intact. diff --git a/src/infra/strkern.h b/src/infra/strkern.h index 8eab06adf..f4479780c 100644 --- a/src/infra/strkern.h +++ b/src/infra/strkern.h @@ -735,4 +735,63 @@ inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& return tail == n - k ? n : k + tail; } +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// 4. appendCleanRun — the run-copy step the emit escapers are built out of +// ═══════════════════════════════════════════════════════════════════════════════════════════════════ +// +// Lane M's shape, folded into this header on 2026-09-10 the moment the tail above stopped being the +// oracle. It lived in a sibling `src/infra/strkern_find.h` for exactly as long as that defect did, and +// that header's own closing note named this as the fold-back: "if strkern.h grows a shipped tail beside +// its oracle, this file collapses into a call to it and the SIMD path comes along for free". It has, so +// it did. (Owner, 2026-09-10: ALL SIMD string kernels live in ONE header.) +// +// Appends the bytes from d[i] up to (not including) the next byte that is IN `set` — the run the caller's +// per-byte switch has no opinion about — and returns the index of that byte, or n when the rest is clean. +// A zero-length run appends nothing, so the caller needs no emptiness test. +// +// Written to sit in a `for`'s INIT and INCREMENT slots: +// for( std::size_t i = appendCleanRun( d, 0, n, set, out ); i < n; i = appendCleanRun( d, i, n, set, out ) ) +// which is why it takes the index rather than a pointer and returns the next one. That placement is not +// cosmetic: the increment expression also runs on `continue`, so an escaper whose switch arms end in +// `continue` (jsonesc::escapeInto) keeps every one of them, and the loop keeps the SINGLE branch it had +// before the rewrite — the run-copy costs the escapers no measured complexity, which is the difference +// between a gated --quality-delta row and none. +// +// It scans with `findByteset`, i.e. with the block loop, not with the scalar twin — measured, on the REAL +// inputs a warm `--top-k=100000` map hands rw::escapeXml (44k-341k calls per corpus, captured with a +// scratch trace build, replayed end to end through escapeXml, best of 9, four independent process runs, +// box load 24-33). Milliseconds for the whole trace, lower is better: +// +// corpus per-byte switch run-copy + scalar scan run-copy + findByteset +// ripwire 1.60-2.00 0.81-1.01 0.71-0.96 −15% vs scalar +// go 6.85-7.62 4.57-4.85 4.74-5.08 +1% (median len 8: 82% of +// calls never reach a block) +// django 8.00-9.12 3.80-4.30 2.97-3.11 −23% vs scalar +// +// Two corpora win, one ties, none loses outside the noise band — so the block loop ships and no caller +// has to choose. The verb-level number is deliberately NOT claimed: escapeXml is ~1% of a warm map here +// (0.82-1.18% by `sample`), so a 15-23% cut in it is ~0.2% of the run and an interleaved whole-verb A/B +// at this box load resolves nothing (it did not: 12 runs a side, medians identical to 0.01 s). +// +// ONE template, not two overloads — a second body differing only in how it spells "append k bytes" is a +// 48-token clone of the first, and --quality-delta says so out loud. The spelling is picked by +// `if constexpr`: std::string (jsonesc's sink) has the (pointer, count) append and it is measurably the +// faster of the two, std::vector (serialize's sink) has only the iterator-pair insert. Both take a +// contiguous-range memcpy underneath; the difference is the length arithmetic libc++ has to redo when it +// is handed iterators instead of a count, and on strings this short that arithmetic is not free. +template< typename Sink > +inline std::size_t appendCleanRun( const char* d, std::size_t i, std::size_t n, const Byteset256& set, Sink& out ) +{ + const std::size_t clean = findByteset( d + i, n - i, set ); + if constexpr( requires { out.append( d + i, clean ); } ) + { + out.append( d + i, clean ); + } + else + { + out.insert( out.end(), d + i, d + i + clean ); + } + return i + clean; +} + } // namespace rw::strkern diff --git a/src/infra/strkern_find.h b/src/infra/strkern_find.h deleted file mode 100644 index 57b230e36..000000000 --- a/src/infra/strkern_find.h +++ /dev/null @@ -1,86 +0,0 @@ -#pragma once - -// strkern_find.h — the SHIPPED byte-set scan, sibling to strkern.h's kernels. -// -// WHY THIS IS NOT `strkern::findByteset`. strkern.h's `findByteset` ends every call — SIMD path or not — -// in `findByteset_scalar`, and that function is deliberately an ORACLE: it re-derives a four-word bitmap -// from the 32-byte set on every call (a 256-iteration loop) using a DIFFERENT representation, precisely -// so a bug in the (b >> 3, b & 7) packing cannot hide behind a reference that shares it. That is exactly -// right for the gate it was written for and exactly wrong for a hot path whose inputs are SHORT: a -// symbol name, a path, a signature. Measured on this box (see the lane's report — 20k strings, best of 5, -// escapeXml end to end), routing escapeXml through `strkern::findByteset` made it 3.5x-7x SLOWER than -// the per-byte switch it replaced on 6..40-byte inputs, and `escapeXml` went from 4.62% of a warm -// `--top-k=100000` map to 22.46% of one. The 256-iteration preamble dominates everything else. -// -// So the shipped scan is this: one pass, one O(1) bit test per byte, no per-call preamble, the SAME -// `strkern::Byteset256` representation (one definition of the set, shared with the oracle that checks it). -// -// AND NO SIMD, ON PURPOSE. A NEON/AVX2 block loop over the set was measured beside this one across -// three length bands (6..40, 60..200, 200..900) and two special-byte densities. It is a wash below -// ~200 bytes — the strings ripwire actually emits — and worth at most ~1.3x on long sparse text, which -// is a slice of a slice: the whole escaper is 4.62% (XML) / 6.34% (JSON) of a warm map on ripwire's own -// tree and under 2% on the go corpus. Duplicating strkern.h's block loop here to chase that would buy a -// clone of another lane's kernel for a fraction of a fraction. The run-copy shape is where the win is -// (1.5x-3x, every band, every density); the scan under it is not. -// -// The headroom is real and recorded rather than taken: if `findByteset_scalar` ever stops being the tail -// of `findByteset` — i.e. if strkern.h grows a shipped tail beside its oracle — this file collapses into -// a call to it and the SIMD path comes along for free. That is the fold-back, and it belongs to the lane -// that owns strkern.h. - -#include "strkern.h" - -#include -#include -#include - -namespace rw::strkern -{ - -// Index of the first byte of [p, p+n) that is IN `set`, or n when none is. Pure, allocation-free, -// locale-independent; n == 0 returns 0. -inline std::size_t findBytesetRun( const char* p, std::size_t n, const Byteset256& set ) noexcept -{ - std::size_t k = 0; - while( k < n && !set.contains( static_cast( p[k] ) ) ) - { - ++k; - } - return k; -} - -// THE RUN-COPY STEP, so that neither escaper grows a shape around it. Appends the bytes from d[i] up to -// (not including) the next byte that is in `set` — the run the caller's per-byte switch has no opinion -// about — and returns the index of that byte, or n when the rest is clean. A zero-length run appends -// nothing, so the caller needs no emptiness test. -// -// Written to sit in a `for`'s INIT and INCREMENT slots: -// for( std::size_t i = appendCleanRun( d, 0, n, set, out ); i < n; i = appendCleanRun( d, i, n, set, out ) ) -// which is why it takes the index rather than a pointer and returns the next one. That placement is not -// cosmetic: the increment expression also runs on `continue`, so an escaper whose switch arms end in -// `continue` (jsonesc::escapeInto) keeps every one of them, and the loop keeps the SINGLE branch it had -// before the rewrite — the run-copy costs the escapers no measured complexity, which is the difference -// between a gated --quality-delta row and none. -// -// ONE template, not two overloads — a second body differing only in how it spells "append k bytes" is a -// 48-token clone of the first, and --quality-delta says so out loud. The spelling is picked by -// `if constexpr`: std::string (jsonesc's sink) has the (pointer, count) append and it is measurably the -// faster of the two, std::vector (serialize's sink) has only the iterator-pair insert. Both take a -// contiguous-range memcpy underneath; the difference is the length arithmetic libc++ has to redo when it -// is handed iterators instead of a count, and on strings this short that arithmetic is not free. -template< typename Sink > -inline std::size_t appendCleanRun( const char* d, std::size_t i, std::size_t n, const Byteset256& set, Sink& out ) -{ - const std::size_t clean = findBytesetRun( d + i, n - i, set ); - if constexpr( requires { out.append( d + i, clean ); } ) - { - out.append( d + i, clean ); - } - else - { - out.insert( out.end(), d + i, d + i + clean ); - } - return i + clean; -} - -} // namespace rw::strkern diff --git a/src/serialize.h b/src/serialize.h index 09da4cc00..dc6500e37 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -15,7 +15,7 @@ #include "redact.h" // deterministic secret redaction of emitted body content (opt-out --no-redact) #include "infra/sortutil.h" // numeric-key radix helpers for rank/file score order #include "infra/jsonesc.h" // F9: jsonesc::utf8SeqLen — the canonical UTF-8-sequence-length core (was duplicated here) -#include "infra/strkern_find.h" // S5: findBytesetRun — the run-copy skip that replaces escapeXml's per-byte switch +#include "infra/strkern.h" // S5: appendCleanRun — the run-copy skip that replaces escapeXml's per-byte switch #include "notes.h" // L3: field-notes NoteIndex — the retrieval-time surfacing lookup (INERT when null) #include "pageview.h" // §P8: pageWindow / pageDisclosure — the shared --limit/--offset contract (packDeps) #include "sarif.h" // R-E (2026-08-17): rootRelativeUri/rootPrefixOf — the same root= single-root-only From c857f27bf1798b0ef3f80e8a0a884f1026570c52 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 20:32:43 -0400 Subject: [PATCH 39/73] test(strkern): two standalone harnesses become one doctest target beside verify_csr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The owner's form for a C++ unit gate is test/verify_csr.cpp / test/verify_pagerank.cpp: DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN, TEST_CASE/CHECK/REQUIRE, a CMake target under -DRIPWIRE_TESTS=ON, doctest vendored at third_party/deps/doctest. test/strkern_harness.cpp and test/emitescape_harness.cpp were the other form — hand-rolled `checkf` printers with their own main and their own PASS counting — and they tested the SAME header from two sides: the escapers are strkern::findByteset's only shipped callers, so a set bug and a scan bug are indistinguishable from a diff. One file, one target. test/verify_strkern.cpp 19 TEST_CASEs, 19 assertions, one per kernel and per escaper arm. CMake ripwire_test_strkern beside ripwire_test_csr: doctest::doctest, the same include paths, added to RIPWIRE_RUNTIME_COMPILE/LINK and RIPWIRE_OWNED_CXX_TARGETS so -DRIPWIRE_ASAN=ON carries the complete G1 stack onto it, `add_test(NAME ripwire.strkern)` so ctest runs it. ARM COUNT, BEFORE AND AFTER, printed by both gates on every run so a lost arm is arithmetic: 14 (strkern_harness) + 4 (emitescape_harness) = 18 -> 19. The extra one is the compiled-path assertion; nothing else was added or dropped. Preserved exactly: both fixed-seed corpora (100k random buffers over four alphabets x lengths 0..300; 222,682 adversarial escaper inputs), the all-256-bytes-at-every-offset-and-length sweep, the every-byte-of-src/-and-docs/ real-text arms, the VERBATIM pre-2026-09-10 tokenizer walkers and the VERBATIM pre-rewrite escapers as oracles, and BOTH mutation controls (-DSTRKERN_MUTATE=1, -DEMITESCAPE_MUTATE_BYTESET=1). Each corpus is walked ONCE in a memoised builder the TEST_CASEs read, so one-assertion-per-test-case costs no extra pass over 100k buffers. The old harnesses' bundled arms became per-kernel probe functions (probeClassMasks / probeFold / probeFoldedEquals / probeFindByte / probeFind3 / probeFindByteset, and probeFile* for the real-text side), which is where splitting was worth doing. The random draws stay inside the walk in the same ORDER, so every arm sees the same corpus it did. THE GATES DRIVE THE TARGET. test/strkerncheck.sh (1) cmake -DRIPWIRE_TESTS=ON -DRIPWIRE_ASAN=ON in a scratch dir + --target ripwire_test_strkern, run whole: the G1 arm for the entire TU, kernels and escapers, with -fno-sanitize-recover=all. (2) direct $CXX -DSTRKERN_MUTATE=1, must fail: 11 of 19 red (was 8, then 9 once the tail fix added the words mutation; 11 now that the escapers route through findByteset, so a kernel mutation reaches them). (3) direct $CXX -arch x86_64 -march=x86-64-v3 under Rosetta 2, 19 assertions green. test/emitescapecheck.sh (A) the same CMake target, `-tc=escape:*`, 4 test cases / 4 assertions. (B) direct $CXX -DEMITESCAPE_MUTATE_BYTESET=1: 168,423 of 222,682 inputs must disagree — same number the standalone harness reported. (C) the every-byte fixture through --for/--expand/--json, unchanged. Arms 2, 3 and B are DIRECT COMPILES on purpose, and the scripts say why: CMake cannot express a second architecture for one target in this tree, and a second full configure to pass one -D would cost a configure to say nothing extra. emitescapecheck builds WITHOUT sanitizers because strkerncheck already runs every test case in this TU under the complete stack; a second sanitized copy would re-prove that at the price of another build. test/binoverridecheck.sh's EXEMPT reason for strkerncheck is updated (it drives a CMake target now, and still never invokes build/ripwire). No new gate file, so test/regression.sh and the published gate count are unchanged — gatecountcheck and manifestcheck green. COST, stated because it went up: strkerncheck 17 s -> 80 s wall / 48 s CPU, emitescapecheck ~15 s -> 27 s wall / 21 s CPU, measured at box load 27. The TU is bigger (it pulls src/serialize.h) and is compiled three times. No pargates.py GATE_BUDGET_SEC entry is added: 48 s of CPU against a 300 s default (x4 on CI's --budget-scale) is 6x headroom, and an entry for it would have been this commit's only gating --quality-delta row. If CI ever shows the rc=124-at-exactly-the-cap signature for either gate, the entry is the remedy and these are the numbers it should carry. --quality-delta: gating=0, preexisting-worse=0, 25 new-symbol rows, nothing acked. One of them was a real clone the tool was right about (classMasksSweep vs probeClassMasks shared a 207-token compare) and is fixed by funnelling both through compareClassMasksWindow. The rest are read and kept, with the reasons at the top of the file: the frozen *Ref oracles must not be restructured to flatter a metric, and the repeated 36-43 token TEST_CASE body IS one-assertion-per-test-case. --test-gate names exactly the three changed gates; untested=0; all three green. Byte-identical 21/21 (3 corpora x 7 surfaces), determinism and xmllint clean — no shipped code moved in this commit. --- CMakeLists.txt | 19 +- test/binoverridecheck.sh | 2 +- test/emitescape_harness.cpp | 407 ----------- test/emitescapecheck.sh | 73 +- test/strkern_harness.cpp | 731 -------------------- test/strkerncheck.sh | 178 +++-- test/verify_strkern.cpp | 1280 +++++++++++++++++++++++++++++++++++ 7 files changed, 1457 insertions(+), 1233 deletions(-) delete mode 100644 test/emitescape_harness.cpp delete mode 100644 test/strkern_harness.cpp create mode 100644 test/verify_strkern.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 28b64ffeb..5ab51a771 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -640,6 +640,19 @@ if(RIPWIRE_TESTS) src/infra/diagnostics.cpp) target_include_directories(ripwire_test_radix PRIVATE src/infra third_party src) add_test(NAME ripwire.radix COMMAND ripwire_test_radix) + + # src/infra/strkern.h's SIMD-vs-scalar parity, src/lexindex.h's tokenizer equivalence, and the three + # emit escapers' byte-identity — one target, driven by test/strkerncheck.sh and test/emitescapecheck.sh + # (each adds its own mutation and cross-arch arms, which are compile flags a second configure cannot + # express in this tree). RIPWIRE_TEST_ROOT is the repo whose src/ and docs/ the real-text arms read, so + # a bare `ctest` is as honest as a gate run; the gates override it with RIPWIRE_ROOT in the environment. + add_executable(ripwire_test_strkern + test/verify_strkern.cpp + src/infra/diagnostics.cpp) + target_include_directories(ripwire_test_strkern PRIVATE src/infra third_party src) + target_compile_definitions(ripwire_test_strkern PRIVATE RIPWIRE_TEST_ROOT="${CMAKE_CURRENT_SOURCE_DIR}") + target_link_libraries(ripwire_test_strkern PRIVATE doctest::doctest) + add_test(NAME ripwire.strkern COMMAND ripwire_test_strkern) endif() # ---- self-profiling build (src/infra/profileScope.h): -DRIPWIRE_PROFILE=ON ---- @@ -726,9 +739,9 @@ set(RIPWIRE_RUNTIME_LINK_TARGETS ripwire_probe ripwire) # the only ones the libstdc++ header exemption below has anything to say about (the grammars are C). set(RIPWIRE_OWNED_CXX_TARGETS ripwire_probe ripwire) if(RIPWIRE_TESTS) - list(APPEND RIPWIRE_RUNTIME_COMPILE_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix) - list(APPEND RIPWIRE_RUNTIME_LINK_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix) - list(APPEND RIPWIRE_OWNED_CXX_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix) + list(APPEND RIPWIRE_RUNTIME_COMPILE_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix ripwire_test_strkern) + list(APPEND RIPWIRE_RUNTIME_LINK_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix ripwire_test_strkern) + list(APPEND RIPWIRE_OWNED_CXX_TARGETS ripwire_test_csr ripwire_test_pagerank ripwire_test_radix ripwire_test_strkern) endif() # G1's `integer` is a CLANG-ONLY UBSan group (unsigned-integer-overflow, implicit-*-truncation, # implicit-integer-sign-change — the defined-but-suspicious conversions, not UB). GCC has no such group diff --git a/test/binoverridecheck.sh b/test/binoverridecheck.sh index 36268692d..ab681ddc2 100755 --- a/test/binoverridecheck.sh +++ b/test/binoverridecheck.sh @@ -110,7 +110,7 @@ EXEMPT = { "portablebuildcheck.sh": "CMake-configure-level gate only; the gate's own banner says 'no ripwire binary needed'", "qschemetripcheck.sh": "greps src/quality.h's tripwire comment against the test/*.sh manifest; pure file check", "radixsimdcheck.sh": "builds its OWN standalone harness binaries per SIMD arm, independent of build/ripwire", - "strkerncheck.sh": "builds its OWN standalone harness binaries per SIMD arm (native, mutated, x86_64 cross), independent of build/ripwire", + "strkerncheck.sh": "drives the CMake target ripwire_test_strkern (test/verify_strkern.cpp) and two direct-compiled SIMD arms (mutated, x86_64 cross) — never invokes build/ripwire", "releaseinstallcheck.sh": "tests install.sh against a FABRICATED release asset/stub server; independent of build/ripwire", "reusefirstworkflowcheck.sh":"checks skills/ripwire-reuse-first/SKILL.md content; pure file check", "ripwirepubliccheck.sh": "checks git-tracked files for leaked private content; pure file/grep check", diff --git a/test/emitescape_harness.cpp b/test/emitescape_harness.cpp deleted file mode 100644 index c17fe8a2d..000000000 --- a/test/emitescape_harness.cpp +++ /dev/null @@ -1,407 +0,0 @@ -// emitescape_harness.cpp — byte-identity harness for the RUN-COPY rewrite of the three emit escapers -// (rw::escapeXml and rw::appendCdataSafe in src/serialize.h, rw::jsonesc::escapeInto in -// src/infra/jsonesc.h). All three are header-only, so this calls them directly rather than diffing a -// whole map, and it is independent of the ripwire binary and of main.cpp. -// -// THE CONTRACT UNDER TEST. The rewrite replaces a per-byte switch with "find the next byte that is IN -// the special set (strkern::findByteset), copy the clean run in one memcpy, handle that one byte with -// the SAME switch, repeat". That is a pure performance change: the emitted bytes must not move, on ANY -// input, including the ones a hand-written byte set is most likely to get wrong. So this harness keeps -// the ORIGINAL per-byte loops verbatim as `*Ref` below and asserts the shipped function agrees with -// them byte-for-byte. The references are frozen copies — if the shipped semantics ever legitimately -// change, the reference changes in the same commit and the gate says so out loud. -// -// Cases proved: -// A every one of the 256 byte values, alone and concatenated in order. -// B a special byte planted at EVERY offset of a 0..96-byte filler string — the block-boundary sweep -// that a 16-byte NEON / 32-byte AVX2 run loop plus its scalar tail must survive. -// C invalid UTF-8: bare continuation, overlong 2/3/4-byte forms, UTF-16 surrogate halves, >U+10FFFF, -// a sequence TRUNCATED at end-of-buffer, and a lone continuation byte as the final byte. -// D valid multibyte (Latin-1 range, CJK, astral) and a UTF-8 BOM, alone and around specials. -// E CDATA: "]]>" at the start, mid, and end of a body, "]]]]>", and a trailing "]]". -// F all four (escapeAngleAmp, validateUtf8) combinations of escapeInto, plus both -// replacementAsTextEscape postures. -// G 200k deterministic fuzz strings over an alphabet biased to the special set. -// MUT a can-go-red arm: a byteset with '<' DROPPED (compiled in with -DEMITESCAPE_MUTATE_BYTESET=1 -// as `escapeXmlMutatedSet`) MUST disagree with the reference. If it agrees, the comparison is -// not looking at what it claims to and the gate is worthless. -// -// Exit 0 = all pass; nonzero = a failure. - -#include "../src/serialize.h" -#include "../src/infra/hashutil.h" -#include "../src/infra/jsonesc.h" - -#include -#include -#include -#include - -using namespace rw; - -static int g_fail = 0; -static int g_checks = 0; - -static void check( bool cond, const char* msg ) -{ - ++g_checks; - if( !cond ) - { - std::printf( " FAIL %s\n", msg ); - g_fail = 1; - } -} - -// ── the frozen per-byte references (verbatim copies of the pre-rewrite loops) ────────────────────────── - -static std::string escapeXmlRef( std::string_view s ) -{ - std::string out; - const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; - const char* d = s.data(); - const std::size_t n = s.size(); - for( std::size_t i = 0; i < n; ) - { - const char c = d[i]; - switch( c ) - { - case '&': put( "&" ); ++i; break; - case '<': put( "<" ); ++i; break; - case '>': put( ">" ); ++i; break; - case '"': put( """ ); ++i; break; - case '\'': put( "'" ); ++i; break; - case '\t': - case '\n': - case '\r': put( xmlControlCharRef( c ) ); ++i; break; - default: - if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } - else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } - else - { - for( int k = 0; k < len; ++k ) - { - out.push_back( d[i + k] ); - } - i += std::size_t( len ); - } - } - } - return out; -} - -static std::string appendCdataSafeRef( std::string_view body ) -{ - std::string safe; - const char* d = body.data(); - const std::size_t n = body.size(); - for( std::size_t i = 0; i < n; ) - { - if( i + 2 < n && d[i] == ']' && d[i + 1] == ']' && d[i + 2] == '>' ) - { safe += "]]]]>"; i += 3; continue; } - const unsigned char c = static_cast( d[i] ); - if( c < 0x80 ) { safe += xmlSafeByte( d[i] ); ++i; } - else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { safe += '?'; ++i; } - else { safe.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } - } - return safe; -} - -static std::string escapeIntoRef( std::string_view s, bool escapeAngleAmp, bool validateUtf8, bool replacementAsTextEscape ) -{ - std::string out; - const char* d = s.data(); - const std::size_t n = s.size(); - std::size_t i = 0; - while( i < n ) - { - const unsigned char c = static_cast( d[i] ); - if( c < 0x80 ) - { - switch( c ) - { - case '"': out += "\\\""; ++i; continue; - case '\\': out += "\\\\"; ++i; continue; - case '\n': out += "\\n"; ++i; continue; - case '\r': out += "\\r"; ++i; continue; - case '\t': out += "\\t"; ++i; continue; - case '<': if( escapeAngleAmp ) { out += "\\u003c"; ++i; continue; } break; - case '>': if( escapeAngleAmp ) { out += "\\u003e"; ++i; continue; } break; - case '&': if( escapeAngleAmp ) { out += "\\u0026"; ++i; continue; } break; - default: break; - } - if( c < 0x20 ) - { char b[ 8 ]; std::snprintf( b, sizeof( b ), "\\u%04x", unsigned( c ) ); out += b; } - else - { - out += char( c ); - } - ++i; - continue; - } - if( !validateUtf8 ) { out += char( c ); ++i; continue; } - const int len = jsonesc::utf8SeqLen( d, i, n ); - if( len == 0 ) - { - if( replacementAsTextEscape ) { out += "\\ufffd"; } - else { out += "\xEF\xBF\xBD"; } - ++i; - } - else { out.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } - } - return out; -} - -// ── the shipped functions, wrapped to the same signature ────────────────────────────────────────────── - -static std::string escapeXmlNew( std::string_view s ) -{ - std::vector buf; - const std::string_view v = escapeXml( s, buf ); - return std::string( v ); -} - -static std::string appendCdataSafeNew( std::string_view s ) -{ - std::string out; - appendCdataSafe( s, out ); - return out; -} - -static std::string escapeIntoNew( std::string_view s, bool a, bool v, bool r ) -{ - std::string out; - jsonesc::escapeInto( s, out, a, v, r ); - return out; -} - -// ── MUT: the same run-copy shape with '<' dropped from the byte set ─────────────────────────────────── -// Deliberately WRONG. Not compiled into anything shipped; it exists so the harness can prove that its -// comparison actually notices a set member going missing (a byteset bug is silent otherwise — the -// output is still well-formed-looking text, just with a raw '<' where an entity belonged). -#if EMITESCAPE_MUTATE_BYTESET -static std::string escapeXmlMutatedSet( std::string_view s ) -{ - std::string out; - const char* d = s.data(); - const std::size_t n = s.size(); - const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; - for( std::size_t i = 0; i < n; ) - { - const char c = d[i]; - switch( c ) - { - // '<' intentionally absent from the set — falls through to the verbatim copy below. - case '&': put( "&" ); ++i; break; - case '>': put( ">" ); ++i; break; - case '"': put( """ ); ++i; break; - case '\'': put( "'" ); ++i; break; - case '\t': - case '\n': - case '\r': put( xmlControlCharRef( c ) ); ++i; break; - default: - if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } - else if( const int len = jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } - else - { - for( int k = 0; k < len; ++k ) { out.push_back( d[i + k] ); } - i += std::size_t( len ); - } - } - } - return out; -} -#endif - -// ── the corpus ──────────────────────────────────────────────────────────────────────────────────────── - -// Deterministic generator, sanitizer-clean by construction: the multiplies go through -// hashutil::multiplyModulo64 (a 128-bit widen and mask) rather than wrapping in 64 bits, because G1 -// compiles these gates with -fsanitize=integer -fno-sanitize-recover=all and a plain `state * k` aborts -// the run on unsigned-integer-overflow. Same reasoning, same shape as test/harnesscommon.h's generator; -// kept local so this TU needs no second include path. Fixed seed => a failure reproduces anywhere. -struct Rng -{ - std::uint64_t state = 0x9E3779B97F4A7C15ull; - std::uint64_t next() noexcept - { - state = rw::hashutil::multiplyModulo64( state, 6364136223846793005ull ) ^ 1442695040888963407ull; - std::uint64_t m = state; - m ^= m >> 33; - m = rw::hashutil::multiplyModulo64( m, 0xFF51AFD7ED558CCDull ); - m ^= m >> 33; - return m; - } -}; - -static void addCase( std::vector& v, std::string s ) { v.push_back( std::move( s ) ); } - -static std::vector buildCorpus() -{ - std::vector cases; - - // A — every byte value alone, and all 256 in order. - std::string all; - for( int b = 0; b < 256; ++b ) - { - addCase( cases, std::string( 1, char( b ) ) ); - all.push_back( char( b ) ); - } - addCase( cases, all ); - addCase( cases, std::string() ); - - // B — a special byte planted at every offset of a filler run, across every length up to two - // 32-byte AVX2 blocks plus a tail. - const char specials[] = { '&', '<', '>', '"', '\'', '\t', '\n', '\r', '\0', '\x0b', '\x1f', '\x7f', - char( 0x80 ), char( 0xC3 ), char( 0xFF ), ']' }; - for( char sp : specials ) - { - for( std::size_t len = 1; len <= 96; ++len ) - { - for( std::size_t at = 0; at < len; at += ( len > 40 ? 7 : 1 ) ) - { - std::string s( len, 'a' ); - s[at] = sp; - addCase( cases, s ); - } - } - } - - // C — invalid UTF-8 shapes. - const char* bad[] = { - "\x80", "\xBF", "\xC0\x80", "\xC1\xBF", "\xC2", "\xE0\x80\x80", "\xE0\x9F\xBF", - "\xED\xA0\x80", "\xED\xBF\xBF", "\xE2\x82", "\xF0\x80\x80\x80", "\xF0\x8F\xBF\xBF", - "\xF4\x90\x80\x80", "\xF5\x80\x80\x80", "\xFE", "\xFF", "\xF0\x9D\x84", - }; - for( const char* b : bad ) - { - std::string s( b ); - addCase( cases, s ); - addCase( cases, "abc" + s ); - addCase( cases, s + "abc" ); - addCase( cases, "abc" + s + "<&>" ); - addCase( cases, std::string( 31, 'x' ) + s ); - addCase( cases, std::string( 32, 'x' ) + s ); - addCase( cases, std::string( 33, 'x' ) + s ); - } - // lone continuation byte as the very last byte of the buffer - addCase( cases, std::string( 40, 'q' ) + "\xBF" ); - addCase( cases, std::string( 40, 'q' ) + "\xE2\x82" ); - - // D — valid multibyte + BOM. - const char* good[] = { "\xC3\xA9", "\xE2\x82\xAC", "\xF0\x9D\x84\x9E", "\xEF\xBB\xBF", "\xEF\xBF\xBD" }; - for( const char* g : good ) - { - std::string s( g ); - addCase( cases, s ); - addCase( cases, s + "<" + s ); - addCase( cases, std::string( 30, 'z' ) + s + std::string( 30, 'z' ) ); - addCase( cases, std::string( 31, 'z' ) + s ); - } - - // E — CDATA close sequences. - addCase( cases, "]]>" ); - addCase( cases, "]]" ); - addCase( cases, "]" ); - addCase( cases, "]]]" ); - addCase( cases, "]]]]>" ); - addCase( cases, "a]]>b" ); - addCase( cases, "]]>]]>" ); - addCase( cases, std::string( 31, 'p' ) + "]]>" ); - addCase( cases, std::string( 32, 'p' ) + "]]>" + std::string( 32, 'p' ) ); - addCase( cases, std::string( 30, 'p' ) + "]]" ); - addCase( cases, "]]\x01>" ); - - // G — deterministic fuzz over an alphabet biased to the special set. - Rng rng; - const std::string alphabet = "abcdefgh<>&\"'\t\n\r]] \x01\x1f\x7f\x80\xC3\xA9\xE2\x82\xAC\xF0\x9D\x84\x9E\xFF"; - for( int k = 0; k < 200000; ++k ) - { - const std::size_t len = std::size_t( rng.next() % 201 ); - std::string s; - s.reserve( len ); - for( std::size_t j = 0; j < len; ++j ) - { - s.push_back( alphabet[ std::size_t( rng.next() % alphabet.size() ) ] ); - } - cases.push_back( std::move( s ) ); - } - return cases; -} - -int main() -{ - const std::vector cases = buildCorpus(); - std::printf( "emitescape_harness: %zu inputs\n", cases.size() ); - - std::size_t xmlBad = 0, cdataBad = 0, jsonBad = 0; - for( const std::string& s : cases ) - { - if( escapeXmlNew( s ) != escapeXmlRef( s ) ) - { - if( xmlBad == 0 ) { std::printf( " first escapeXml mismatch, len=%zu\n", s.size() ); } - ++xmlBad; - } - if( appendCdataSafeNew( s ) != appendCdataSafeRef( s ) ) - { - if( cdataBad == 0 ) { std::printf( " first appendCdataSafe mismatch, len=%zu\n", s.size() ); } - ++cdataBad; - } - for( int mode = 0; mode < 8; ++mode ) - { - const bool a = ( mode & 1 ) != 0; - const bool v = ( mode & 2 ) != 0; - const bool r = ( mode & 4 ) != 0; - if( escapeIntoNew( s, a, v, r ) != escapeIntoRef( s, a, v, r ) ) - { - if( jsonBad == 0 ) { std::printf( " first escapeInto mismatch, mode=%d len=%zu\n", mode, s.size() ); } - ++jsonBad; - } - } - } - check( xmlBad == 0, "escapeXml byte-identical to the frozen per-byte reference" ); - check( cdataBad == 0, "appendCdataSafe byte-identical to the frozen per-byte reference" ); - check( jsonBad == 0, "escapeInto byte-identical to the frozen per-byte reference (8 flag combos)" ); - if( xmlBad ) { std::printf( " escapeXml mismatches: %zu\n", xmlBad ); } - if( cdataBad ) { std::printf( " appendCdataSafe mismatches: %zu\n", cdataBad ); } - if( jsonBad ) { std::printf( " escapeInto mismatches: %zu\n", jsonBad ); } - - // scrub-disclosure predicate must keep agreeing with what the escapers actually DO (§B12.7): the - // lossy-tell is derived from the same byte classes the run loop now skips over in bulk. - std::size_t lossyBad = 0; - for( const std::string& s : cases ) - { - const bool lossy = xmlScrubIsLossy( s ); - const bool cdataHit = appendCdataSafeRef( s ) != std::string( s ) && true; - (void)cdataHit; - // a lossy input is exactly one whose escaped form contains '?' or a substituted space that the - // input did not have; assert the cheap direction: not-lossy ⇒ no '?' introduced. - if( !lossy ) - { - std::string ref = appendCdataSafeRef( s ); - std::string plain( s ); - // appendCdataSafe only splits ]]> on non-lossy input; strip that expansion before comparing - std::string expanded; - for( std::size_t i = 0; i < plain.size(); ) - { - if( i + 2 < plain.size() && plain[i] == ']' && plain[i + 1] == ']' && plain[i + 2] == '>' ) - { expanded += "]]]]>"; i += 3; } - else { expanded += plain[i]; ++i; } - } - if( ref != expanded ) { ++lossyBad; } - } - } - check( lossyBad == 0, "xmlScrubIsLossy(false) really means appendCdataSafe moved no byte" ); - -#if EMITESCAPE_MUTATE_BYTESET - std::size_t mutDiff = 0; - for( const std::string& s : cases ) - { - if( escapeXmlMutatedSet( s ) != escapeXmlRef( s ) ) { ++mutDiff; } - } - check( mutDiff > 0, "MUT: a byteset missing '<' DISAGREES with the reference (the gate can go red)" ); - std::printf( " MUT: %zu of %zu inputs differ\n", mutDiff, cases.size() ); -#endif - - std::printf( "emitescape_harness: %d checks, %s\n", g_checks, g_fail ? "FAIL" : "ALL PASS" ); - return g_fail; -} diff --git a/test/emitescapecheck.sh b/test/emitescapecheck.sh index f1bc10c2e..a95624b42 100755 --- a/test/emitescapecheck.sh +++ b/test/emitescapecheck.sh @@ -5,20 +5,26 @@ # WHY A HARNESS AND NOT A GOLDEN DIFF. The rewrite is "find the next byte in the special set with # strkern::findByteset, memcpy the clean run, handle that one byte with the SAME switch". Nothing in a # golden map exercises the inputs that shape gets wrong — an escaper is only interesting on the bytes a -# repo does not normally contain. So the harness (test/emitescape_harness.cpp) keeps the ORIGINAL -# per-byte loops verbatim as `*Ref` and asserts byte-identity over an adversarial corpus: every one of -# the 256 byte values; a special byte at EVERY offset of a filler run up to two 32-byte AVX2 blocks +# repo does not normally contain. So the `escape:` TEST_CASEs of test/verify_strkern.cpp keep the +# ORIGINAL per-byte loops verbatim as `*Ref` and assert byte-identity over an adversarial corpus: every +# one of the 256 byte values; a special byte at EVERY offset of a filler run up to two 32-byte AVX2 blocks # (the block-boundary sweep a SIMD run loop plus its scalar tail must survive); overlongs, surrogate # halves, >U+10FFFF, truncated sequences, a lone continuation byte as the final byte of the buffer, a # BOM; "]]>" at the start/middle/end and "]]]]>"; all eight escapeInto flag combinations; and 200k # deterministic fuzz strings over an alphabet biased to the special set. # +# The arms live in the SAME doctest target as the strkern kernel arms (CMake `ripwire_test_strkern`, +# 2026-09-10) because they test the same header from the other side: the escapers are findByteset's only +# shipped callers, and a set bug and a scan bug are indistinguishable from a diff. This gate selects them +# with doctest's own filter (`-tc=escape:*`); test/strkerncheck.sh runs the whole target, which is why +# the sanitized build lives there and this gate does not pay for a second copy of it. +# # ARMS -# (A) harness compiles and passes — the shipped escapers agree with the frozen references. -# (B) CAN-GO-RED: the same harness recompiled with -DEMITESCAPE_MUTATE_BYTESET=1, which adds a -# byteset with '<' DROPPED. That build asserts the mutant DISAGREES with the reference. A -# comparison that could not see a missing set member would report zero differences and this arm -# would fail — which is the point: it proves arm (A) is looking at what it claims to. +# (A) the target's escape: arms pass — the shipped escapers agree with the frozen per-byte references. +# (B) CAN-GO-RED: the same target recompiled with -DEMITESCAPE_MUTATE_BYTESET=1, which adds a byteset +# with '<' DROPPED. That build asserts the mutant DISAGREES with the reference. A comparison that +# could not see a missing set member would report zero differences and this arm would fail — which +# is the point: it proves arm (A) is looking at what it claims to. # (C) END TO END: a fixture tree (in a temp dir, NEVER inside the repo — see the # "gate fixture is the live repo" trap) whose doc-comment carries every byte value 0x01..0xFF # except '\n'. The map of that tree must pipe clean through `xmllint --noout` (G4), and the @@ -38,34 +44,53 @@ no(){ printf ' FAIL %s\n' "$*"; fail=1; } . "$ROOT/scripts/cxxstd.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" -HARNESS="$ROOT/test/emitescape_harness.cpp" +SRC="$ROOT/test/verify_strkern.cpp" WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT -echo "emitescapecheck: CXX=$CXX BIN=$BIN" +echo "emitescapecheck: CXX=$CXX BIN=$BIN target=ripwire_test_strkern -tc=escape:*" -compile_arm() # $1=output $2...=extra flags +# doctest's own tally line is the arm count. LEGACY_ESCAPE_ARMS is what the standalone +# test/emitescape_harness.cpp carried before it became TEST_CASEs (2026-09-10); the gate prints both so a +# lost arm is arithmetic, not a feeling. +LEGACY_ESCAPE_ARMS=4 +read_counts() # $1 = log; sets CASES, ASSERTS, ASSERTS_FAIL { - local out="$1"; shift - "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra "$@" \ - -I"$ROOT/src/infra" -I"$ROOT/third_party" -I"$ROOT/src" \ - "$HARNESS" "$ROOT/src/infra/diagnostics.cpp" -o "$out" 2> "$WORK/cc.log" + CASES="$( sed -n 's/^\[doctest\] test cases: *\([0-9][0-9]*\) .*/\1/p' "$1" | tail -1 )" + ASSERTS="$( sed -n 's/^\[doctest\] assertions: *\([0-9][0-9]*\) .*/\1/p' "$1" | tail -1 )" + ASSERTS_FAIL="$( sed -n 's/.*| *\([0-9][0-9]*\) failed |$/\1/p' "$1" | tail -1 )" + : "${CASES:=0}" "${ASSERTS:=0}" "${ASSERTS_FAIL:=1}" } -# ── (A) the shipped escapers vs the frozen per-byte references ──────────────────────────────────────── -if compile_arm "$WORK/plain"; then - if "$WORK/plain" > "$WORK/plain.out" 2>&1; then - ok "escapers byte-identical to the frozen per-byte references over the adversarial corpus" - sed -n 's/^/ /p' "$WORK/plain.out" | head -4 +# ── (A) the shipped escapers vs the frozen per-byte references, through the CMake target ────────────── +# FETCHCONTENT_FULLY_DISCONNECTED=ON because every dependency is vendored: a gate must not reach the +# network. No -DRIPWIRE_ASAN=ON here — test/strkerncheck.sh builds this same TU under the complete G1 +# stack and runs every one of its test cases, so a second sanitized copy would re-prove that at the price +# of another build. +if ! cmake -S "$ROOT" -B "$WORK/cmb" -DRIPWIRE_TESTS=ON -DFETCHCONTENT_FULLY_DISCONNECTED=ON \ + > "$WORK/cfg.log" 2>&1; then + no "cmake configure (-DRIPWIRE_TESTS=ON) failed"; tail -20 "$WORK/cfg.log" | sed 's/^/ /' +elif ! cmake --build "$WORK/cmb" --target ripwire_test_strkern -j 2 > "$WORK/build.log" 2>&1; then + no "ripwire_test_strkern failed to build"; tail -30 "$WORK/build.log" | sed 's/^/ /' +elif RIPWIRE_ROOT="$ROOT" "$WORK/cmb/ripwire_test_strkern" -tc="escape:*" > "$WORK/plain.out" 2>&1; then + read_counts "$WORK/plain.out" + if [ "$ASSERTS" -lt "$LEGACY_ESCAPE_ARMS" ]; then + no "only $ASSERTS escape: assertions ran; the harness this replaced carried $LEGACY_ESCAPE_ARMS — an arm was lost" else - no "harness reported a mismatch"; sed 's/^/ /' "$WORK/plain.out" | head -20 + ok "escapers byte-identical to the frozen per-byte references over the adversarial corpus ($CASES test cases / $ASSERTS assertions; was $LEGACY_ESCAPE_ARMS standalone arms)" fi + grep -E '^\[doctest\] (test cases|assertions):' "$WORK/plain.out" | sed 's/^/ /' else - no "harness failed to compile"; sed 's/^/ /' "$WORK/cc.log" | head -20 + no "the escape: arms reported a mismatch"; sed 's/^/ /' "$WORK/plain.out" | head -30 fi # ── (B) can-go-red: a byteset with '<' dropped must be VISIBLE to the comparison ─────────────────────── -if compile_arm "$WORK/mut" -DEMITESCAPE_MUTATE_BYTESET=1; then - if "$WORK/mut" > "$WORK/mut.out" 2>&1; then +# A compile flag, not a build type: a second CMake configure to pass one -D would cost a configure to say +# nothing extra, so this arm compiles the same source directly the way the pre-doctest gate did. +if "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra -DEMITESCAPE_MUTATE_BYTESET=1 \ + -I"$ROOT/src/infra" -I"$ROOT/third_party" -I"$ROOT/src" -I"$ROOT/third_party/deps/doctest" \ + -DRIPWIRE_TEST_ROOT="\"$ROOT\"" \ + "$SRC" "$ROOT/src/infra/diagnostics.cpp" -o "$WORK/mut" 2> "$WORK/cc.log"; then + if RIPWIRE_ROOT="$ROOT" "$WORK/mut" -tc="escape:*" > "$WORK/mut.out" 2>&1; then ok "MUT arm: a byteset missing '<' is detected (the comparison can go red)" grep -n 'MUT:' "$WORK/mut.out" | sed 's/^/ /' else diff --git a/test/strkern_harness.cpp b/test/strkern_harness.cpp deleted file mode 100644 index 4940ccf24..000000000 --- a/test/strkern_harness.cpp +++ /dev/null @@ -1,731 +0,0 @@ -// strkern_harness.cpp — SIMD-vs-scalar parity gate for src/infra/strkern.h, plus the tokenizer -// equivalence arm that pins lexindex.h's mask-driven walkers against the byte-at-a-time state machines -// they replaced. -// -// A classMasks — per-byte [A-Z]/[a-z]/[0-9]/alnum bitmasks over one block, vector path vs -// the range-test oracle, for EVERY length 0..kBlockBytes. -// B lowerFoldAscii — in-place A-Z fold, vector vs SWAR-scalar, on buffers that straddle every -// block boundary. -// C lowerFoldedEquals — folded compare vs the scalar twin, including every single-byte difference -// position and the case-only differences that are the point of the kernel. -// D findByte / find3 — first-occurrence scans vs the scalar twins AND vs a naive memchr/memcmp -// oracle, needle present and absent, matches at 0 / at n-1 / straddling. -// E findByteset — 256-bit set scan; sets built to hit the (b>>3, b&7) packing's seams -// (empty, full, the 0x80 boundary, one byte only, the XML-escape set). -// F tokenizer equivalence — forEachLexSubtoken / forEachLexSubtokenHashed as shipped vs VERBATIM -// copies of the pre-2026-09-10 byte-at-a-time walkers kept in this file. -// Every (start, end) span and every fused hash must be identical, over the -// random corpus AND over every byte of src/ and docs/. -// -// Corpora: (1) a fixed-seed random sweep — 100k buffers, lengths 0..300, drawn from four alphabets -// (identifier-ish, full ASCII, high-bit/UTF-8, and a camel/acronym-dense generator that manufactures the -// exact seams the tokenizer rule turns on); (2) every regular file under src/ and docs/ of the repo root -// given as argv[1], read whole. Real text is not optional here: the random arms cannot produce the -// distribution of `ACRONYMWord`, `snake_case` and `//` runs that the shipped rule was tuned on. -// -// NON-VACUITY: the banner prints the compiled path (`strkern path: NEON|AVX2|scalar`). On arm64/x86_64 the -// gate script REQUIRES a vector path — a scalar-only build there would compare the oracle to itself. -// CAN-GO-RED: compiling with -DSTRKERN_MUTATE=1 perturbs the SIMD tables only; the gate script proves the -// harness fails under it, so a green run means the parity assertions actually bind. -// -// Exit 0 = all pass; nonzero = failure. - -#include "../src/infra/strkern.h" -#include "../src/lexindex.h" -#include "harnesscommon.h" // checkf / g_fail / DeterministicRng — shared with the other SIMD harnesses - -#include -#include -#include -#include -#include -#include - -namespace sk = rw::strkern; - -// ============================================================================ -// corpora -// ============================================================================ - -// four alphabets, each aimed at a different failure mode -enum class Alphabet -{ - Identifier, // [A-Za-z0-9_] — the tokenizer's natural food - FullAscii, // 0x00..0x7F — every separator, every nibble-table seam ('@' '[' '`' '{' ':' '/') - HighBit, // 0x00..0xFF — proves the >= 0x80 half is a separator and never folds - CamelDense // manufactured camel / ACRONYMWord / digit seams at high density -}; - -static void drawBuffer( DeterministicRng& gen, Alphabet alpha, std::size_t n, std::string& out ) -{ - static const char kIdent[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; - out.clear(); - out.reserve( n ); - while( out.size() < n ) - { - const std::uint64_t r = gen.next(); - switch( alpha ) - { - case Alphabet::Identifier: - out.push_back( kIdent[ r % ( sizeof( kIdent ) - 1 ) ] ); - break; - case Alphabet::FullAscii: - out.push_back( char( r & 0x7F ) ); - break; - case Alphabet::HighBit: - out.push_back( char( r & 0xFF ) ); - break; - case Alphabet::CamelDense: - { - // a short run of one class, then switch — so seams land every 1..4 bytes - const int kind = int( r & 3 ); - const std::size_t runLen = 1 + std::size_t( ( r >> 2 ) & 3 ); - for( std::size_t j = 0; j < runLen && out.size() < n; ++j ) - { - const std::uint64_t s = gen.next(); - switch( kind ) - { - case 0: out.push_back( char( 'A' + ( s % 26 ) ) ); break; - case 1: out.push_back( char( 'a' + ( s % 26 ) ) ); break; - case 2: out.push_back( char( '0' + ( s % 10 ) ) ); break; - default: out.push_back( ( s & 1 ) ? '_' : ' ' ); break; - } - } - break; - } - } - } - out.resize( n ); -} - -// every regular file under /src and /docs, read whole -static void loadRepoText( const char* root, std::vector& outFiles, std::vector& outNames ) -{ - for( const char* sub : { "src", "docs" } ) - { - const std::filesystem::path dir = std::filesystem::path( root ) / sub; - std::error_code ec; - if( !std::filesystem::is_directory( dir, ec ) ) - { - continue; - } - for( std::filesystem::recursive_directory_iterator it( dir, ec ), end; it != end && !ec; it.increment( ec ) ) - { - if( !it->is_regular_file( ec ) ) - { - continue; - } - std::FILE* fp = std::fopen( it->path().string().c_str(), "rb" ); - if( fp == nullptr ) - { - continue; - } - std::string bytes; - char buf[ 65536 ]; - std::size_t got = 0; - while( ( got = std::fread( buf, 1, sizeof( buf ), fp ) ) > 0 ) - { - bytes.append( buf, got ); - } - std::fclose( fp ); - outFiles.push_back( std::move( bytes ) ); - outNames.push_back( it->path().string() ); - } - } -} - -// ============================================================================ -// Arm A — classMasks -// ============================================================================ - -static bool masksEqual( const sk::Masks& a, const sk::Masks& b ) -{ - return a.alnum == b.alnum && a.upper == b.upper && a.lower == b.lower && a.digit == b.digit; -} - -// Every length 0..kBlockBytes over one buffer, vector vs oracle. Returns the first failing (length, -// offset) as a message, or an empty string. -static std::string classMasksSweep( const std::string& text ) -{ - for( std::size_t off = 0; off < text.size(); ++off ) - { - const std::size_t avail = text.size() - off; - const std::size_t maxN = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; - for( std::size_t n = 0; n <= maxN; ++n ) - { - sk::Masks got{}, want{}; - sk::classMasks( text.data() + off, n, got ); - sk::classMasks_scalar( text.data() + off, n, want ); - if( !masksEqual( got, want ) ) - { - char msg[ 256 ]; - std::snprintf( msg, sizeof( msg ), - "off=%zu n=%zu got(a=%08x u=%08x l=%08x d=%08x) want(a=%08x u=%08x l=%08x d=%08x)", - off, n, got.alnum, got.upper, got.lower, got.digit, - want.alnum, want.upper, want.lower, want.digit ); - return msg; - } - } - } - return {}; -} - -// the whole byte alphabet, one byte per position, so no class boundary can go unvisited -static void armAllBytes() -{ - std::string every; - for( unsigned b = 0; b < 256u; ++b ) - { - every.push_back( char( b ) ); - } - const std::string fail = classMasksSweep( every ); - checkf( fail.empty(), "A1 classMasks over all 256 byte values, every offset and length%s%s", - fail.empty() ? "" : " — ", fail.c_str() ); - - // the class definition itself, one byte at a time, against the shipped lexindex predicate set - bool defOk = true; - for( unsigned b = 0; b < 256u; ++b ) - { - const char c = char( b ); - sk::Masks m{}; - sk::classMasks( &c, 1, m ); - const bool wantUpper = b >= 'A' && b <= 'Z'; - const bool wantLower = b >= 'a' && b <= 'z'; - const bool wantDigit = b >= '0' && b <= '9'; - defOk = defOk && ( ( m.upper & 1u ) != 0 ) == wantUpper && ( ( m.lower & 1u ) != 0 ) == wantLower - && ( ( m.digit & 1u ) != 0 ) == wantDigit - && ( ( m.alnum & 1u ) != 0 ) == ( wantUpper || wantLower || wantDigit ); - } - checkf( defOk, "A2 classMasks single-byte classes match [A-Z]/[a-z]/[0-9] exactly (bytes >= 0x80 are separators)" ); -} - -// ============================================================================ -// Arm F — the tokenizer, and the VERBATIM pre-change walkers it must equal -// ============================================================================ - -// Kept byte-for-byte as they stood at 05f4b892 (src/lexindex.h:130 and :201) so this arm compares the new -// mask-driven walkers against the OLD code, not against a paraphrase of it. Do not "clean these up". - -template< class EmitFn > -static void refForEachLexSubtoken( std::string_view text, EmitFn&& emit ) -{ - constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); - std::size_t tokStartByte = kNoTokenByte; - bool prevUpper = false; - for( std::size_t k = 0; k < text.size(); ++k ) - { - const unsigned char c = static_cast< unsigned char >( text[ k ] ); - const bool upper = c >= 'A' && c <= 'Z'; - const bool lower = c >= 'a' && c <= 'z'; - const bool digit = c >= '0' && c <= '9'; - if( !upper && !lower && !digit ) - { - if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k ); tokStartByte = kNoTokenByte; } - prevUpper = false; - continue; - } - if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) - { - emit( tokStartByte, k ); - tokStartByte = k; - } - if( tokStartByte == kNoTokenByte ) - { - tokStartByte = k; - } - prevUpper = upper; - } - if( tokStartByte != kNoTokenByte ) - { - emit( tokStartByte, text.size() ); - } -} - -template< class EmitFn > -static void refForEachLexSubtokenHashed( std::string_view text, EmitFn&& emit ) -{ - constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); - constexpr std::uint64_t kFnvBasis = 1469598103934665603ull; - std::size_t tokStartByte = kNoTokenByte; - std::uint64_t h = kFnvBasis; - bool prevUpper = false; - const auto mix = [ & ]( unsigned char c ) noexcept { h = rw::hashutil::fnv1aAbsorb( h, char( rw::lexLowerByte( c ) ) ); }; - const auto beginToken = [ & ]( unsigned char c, std::size_t k ) noexcept - { - tokStartByte = k; - h = kFnvBasis; - mix( c ); - }; - for( std::size_t k = 0; k < text.size(); ++k ) - { - const unsigned char c = static_cast< unsigned char >( text[ k ] ); - const bool upper = c >= 'A' && c <= 'Z'; - const bool lower = c >= 'a' && c <= 'z'; - const bool digit = c >= '0' && c <= '9'; - if( !upper && !lower && !digit ) - { - if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k, h ); tokStartByte = kNoTokenByte; } - prevUpper = false; - continue; - } - if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) - { - emit( tokStartByte, k, h ); - beginToken( c, k ); - prevUpper = true; - continue; - } - if( tokStartByte == kNoTokenByte ) { beginToken( c, k ); prevUpper = upper; continue; } - mix( c ); - prevUpper = upper; - } - if( tokStartByte != kNoTokenByte ) - { - emit( tokStartByte, text.size(), h ); - } -} - -struct Tok -{ - std::size_t start = 0; - std::size_t end = 0; - std::uint64_t hash = 0; -}; - -static void collectRef( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - refForEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) - { - out.push_back( { s, e, h } ); - } ); -} - -static void collectNew( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - rw::forEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) - { - out.push_back( { s, e, h } ); - } ); -} - -// spans only (the hash-free walker) — a separate list, because the two shipped walkers are separate code -static void collectRefSpans( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - refForEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); -} - -static void collectNewSpans( std::string_view text, std::vector< Tok >& out ) -{ - out.clear(); - rw::forEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { out.push_back( { s, e, 0 } ); } ); -} - -// Compare all four lists for one text. Returns "" when identical, else the first divergence. -static std::string tokenizerDiff( std::string_view text ) -{ - static std::vector< Tok > refH, newH, refS, newS; - collectRef( text, refH ); - collectNew( text, newH ); - collectRefSpans( text, refS ); - collectNewSpans( text, newS ); - - char msg[ 384 ]; - if( refS.size() != newS.size() ) - { - std::snprintf( msg, sizeof( msg ), "span COUNT %zu vs %zu (len=%zu)", refS.size(), newS.size(), text.size() ); - return msg; - } - for( std::size_t i = 0; i < refS.size(); ++i ) - { - if( refS[ i ].start != newS[ i ].start || refS[ i ].end != newS[ i ].end ) - { - std::snprintf( msg, sizeof( msg ), "span #%zu [%zu,%zu) vs [%zu,%zu) (len=%zu)", i, - refS[ i ].start, refS[ i ].end, newS[ i ].start, newS[ i ].end, text.size() ); - return msg; - } - } - if( refH.size() != newH.size() ) - { - std::snprintf( msg, sizeof( msg ), "hashed COUNT %zu vs %zu (len=%zu)", refH.size(), newH.size(), text.size() ); - return msg; - } - for( std::size_t i = 0; i < refH.size(); ++i ) - { - if( refH[ i ].start != newH[ i ].start || refH[ i ].end != newH[ i ].end || refH[ i ].hash != newH[ i ].hash ) - { - std::snprintf( msg, sizeof( msg ), "hashed #%zu [%zu,%zu)#%016llx vs [%zu,%zu)#%016llx (len=%zu)", i, - refH[ i ].start, refH[ i ].end, ( unsigned long long )refH[ i ].hash, - newH[ i ].start, newH[ i ].end, ( unsigned long long )newH[ i ].hash, text.size() ); - return msg; - } - // and the fused hash must still equal the standalone lexSubtokenHash of the same span - const std::uint64_t standalone = rw::lexSubtokenHash( text.data() + newH[ i ].start, newH[ i ].end - newH[ i ].start ); - if( standalone != newH[ i ].hash ) - { - std::snprintf( msg, sizeof( msg ), "fused hash #%zu %016llx != lexSubtokenHash %016llx", i, - ( unsigned long long )newH[ i ].hash, ( unsigned long long )standalone ); - return msg; - } - // ... and the hash-free walker's spans must be the same spans - if( refS[ i ].start != newH[ i ].start || refS[ i ].end != newH[ i ].end ) - { - std::snprintf( msg, sizeof( msg ), "walker disagreement #%zu [%zu,%zu) vs [%zu,%zu)", i, - refS[ i ].start, refS[ i ].end, newH[ i ].start, newH[ i ].end ); - return msg; - } - } - return {}; -} - -// The hand-written seam table from docs/EVALS.md §4 — the cases the acronym rule exists for, spelled out -// so a failure names the input rather than a random offset. -static void armTokenizerSeams() -{ - static const char* kCases[] = { - "", "a", "A", "aB", "Ab", "AB", "ABc", "aBc", "MCP", "MCP2Server", "HTTPServer", "IOError", - "XMLHttpRequest", "_max_speed", "updateCollisionPositionVelocity", "foo bar", " ", "__", - "A1B2C3", "camelCASE", "CASEcamel", "endsWithUPPER", "x", "0", "9a", "a9", "Z", "aZ", "ZZa", - "ZZZZZZZZZZZZZZZZZZZZa", // acronym run straddling a 16-byte block - "aaaaaaaaaaaaaaaBcccccccccccccccDeeeeeeeeeeeeeeeF", // camel seam at 15/31/47 - "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaBc", // camel seam exactly at 32 - "ABCDEFGHIJKLMNOPa", "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFa", // acronym seam at 16 and at 32 - "____________________abc", "abc____________________", - }; - bool ok = true; - std::string firstFail; - for( const char* c : kCases ) - { - const std::string d = tokenizerDiff( c ); - if( !d.empty() && firstFail.empty() ) - { - firstFail = std::string( "\"" ) + c + "\": " + d; - ok = false; - } - } - checkf( ok, "F1 tokenizer equals the pre-change walker on the %zu registered seam cases%s%s", - sizeof( kCases ) / sizeof( kCases[ 0 ] ), ok ? "" : " — ", firstFail.c_str() ); -} - -// ============================================================================ -// main -// ============================================================================ - -int main( int argc, char** argv ) -{ - const char* root = argc > 1 ? argv[ 1 ] : "."; - std::printf( "strkern: path=%s block=%zu root=%s\n", sk::kPathName, sk::kBlockBytes, root ); - std::printf( "strkern path: %s\n", sk::kPathName ); - - armAllBytes(); - armTokenizerSeams(); - - // ── the fixed-seed random sweep ────────────────────────────────────────────────────────────────── - DeterministicRng gen{ 0x5DEECE66Dull }; - std::string buf, folded, foldedRef, lowered; - std::string classFail, foldFail, eqFail, findFail, tokFail; - std::size_t bufferCount = 0; - - // one byteset per shape the (b>>3, b&7) packing could get wrong - sk::Byteset256 setEmpty, setFull, setOne, setHighOnly, setXml; - for( unsigned b = 0; b < 256u; ++b ) - { - setFull.add( static_cast< unsigned char >( b ) ); - } - setOne.add( 'q' ); - setHighOnly.addRange( 0x80, 0xFF ); - for( char c : { '&', '<', '>', '"', '\'', '\t', '\n', '\r' } ) - { - setXml.add( static_cast< unsigned char >( c ) ); - } - setXml.addRange( 0x80, 0xFF ); - const sk::Byteset256* kSets[] = { &setEmpty, &setFull, &setOne, &setHighOnly, &setXml }; - const char* kSetNames[] = { "empty", "full", "one", "high", "xml" }; - - // E0 — Byteset256 now stores TWO derivations of the same set (bits for the SIMD table lookups, words - // for the scalar tail's O(1) test), both written by add(). Nothing else in the header would notice one - // of them going stale, so this arm reads both back for every set and every byte value. - std::string repFail; - for( std::size_t si = 0; si < 5 && repFail.empty(); ++si ) - { - std::uint64_t rederived[ 4 ] = { 0, 0, 0, 0 }; - for( unsigned b = 0; b < 256u; ++b ) - { - const unsigned char c = static_cast< unsigned char >( b ); - if( kSets[ si ]->contains( c ) ) - { - rederived[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); - } - if( kSets[ si ]->contains( c ) != kSets[ si ]->containsWord( c ) ) - { - char msg[ 128 ]; - std::snprintf( msg, sizeof( msg ), "set=%s byte=%02x bits=%d words=%d", kSetNames[ si ], b, - int( kSets[ si ]->contains( c ) ), int( kSets[ si ]->containsWord( c ) ) ); - repFail = msg; - } - } - for( int w = 0; w < 4 && repFail.empty(); ++w ) - { - if( rederived[ w ] != kSets[ si ]->words[ w ] ) - { - char msg[ 160 ]; - std::snprintf( msg, sizeof( msg ), "set=%s word[%d] stored=%016llx rederived=%016llx", - kSetNames[ si ], w, ( unsigned long long )kSets[ si ]->words[ w ], - ( unsigned long long )rederived[ w ] ); - repFail = msg; - } - } - } - checkf( repFail.empty(), "E0 Byteset256 carries two agreeing representations (bits vs words, 5 sets x 256 bytes)%s%s", - repFail.empty() ? "" : " — ", repFail.c_str() ); - - for( int iter = 0; iter < 100000; ++iter ) - { - const Alphabet alpha = Alphabet( iter & 3 ); - const std::size_t n = std::size_t( gen.next() % 301u ); // 0..300, straddles 16/32 repeatedly - drawBuffer( gen, alpha, n, buf ); - ++bufferCount; - - // A — classMasks at every offset/length that fits in one block, but only on a slice (the full - // O(n * block) sweep on 100k buffers would dominate the gate's runtime) - if( classFail.empty() ) - { - const std::size_t probeOff = n == 0 ? 0 : std::size_t( gen.next() % n ); - const std::size_t avail = n - probeOff; - const std::size_t maxN = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; - for( std::size_t m = 0; m <= maxN && classFail.empty(); ++m ) - { - sk::Masks got{}, want{}; - sk::classMasks( buf.data() + probeOff, m, got ); - sk::classMasks_scalar( buf.data() + probeOff, m, want ); - if( !masksEqual( got, want ) ) - { - char msg[ 256 ]; - std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d off=%zu n=%zu", iter, int( alpha ), probeOff, m ); - classFail = msg; - } - } - } - - // B — lowerFoldAscii, vector vs SWAR scalar, in place - if( foldFail.empty() ) - { - folded = buf; - foldedRef = buf; - sk::lowerFoldAscii( folded.data(), folded.size() ); - sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); - if( folded != foldedRef ) - { - char msg[ 128 ]; - std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d n=%zu", iter, int( alpha ), n ); - foldFail = msg; - } - // and against the definition, byte by byte - for( std::size_t k = 0; k < n && foldFail.empty(); ++k ) - { - const unsigned char c = static_cast< unsigned char >( buf[ k ] ); - const unsigned char want = ( c >= 'A' && c <= 'Z' ) ? static_cast< unsigned char >( c + 0x20 ) : c; - if( static_cast< unsigned char >( folded[ k ] ) != want ) - { - char msg[ 128 ]; - std::snprintf( msg, sizeof( msg ), "definition iter=%d k=%zu byte=%02x", iter, k, c ); - foldFail = msg; - } - } - } - - // C — lowerFoldedEquals: equal case, and every single-byte perturbation of one random position - if( eqFail.empty() && n > 0 ) - { - lowered = buf; - sk::lowerFoldAscii_scalar( lowered.data(), lowered.size() ); - if( !sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) - || !sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) - { - eqFail = "self-compare returned false"; - } - const std::size_t at = std::size_t( gen.next() % n ); - const char old = lowered[ at ]; - lowered[ at ] = char( static_cast< unsigned char >( old ) ^ 0x01 ); - if( eqFail.empty() - && sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) != sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) - { - char msg[ 128 ]; - std::snprintf( msg, sizeof( msg ), "perturbed iter=%d at=%zu n=%zu", iter, at, n ); - eqFail = msg; - } - lowered[ at ] = old; - } - - // D/E — the find kernels vs their scalar twins vs a naive oracle - if( findFail.empty() ) - { - const char needle = char( gen.next() & 0xFF ); - const std::size_t gotB = sk::findByte( buf.data(), n, needle ); - const std::size_t refB = sk::findByte_scalar( buf.data(), n, needle ); - std::size_t naive = n; - for( std::size_t k = 0; k < n; ++k ) - { - if( buf[ k ] == needle ) { naive = k; break; } - } - if( gotB != refB || gotB != naive ) - { - char msg[ 160 ]; - std::snprintf( msg, sizeof( msg ), "findByte iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, gotB, refB, naive, n ); - findFail = msg; - } - - // find3: half the time plant the needle so a HIT is exercised, half the time draw at random - char needle3[ 3 ] = { char( gen.next() & 0xFF ), char( gen.next() & 0xFF ), char( gen.next() & 0xFF ) }; - if( n >= 3 && ( gen.next() & 1 ) ) - { - const std::size_t at = std::size_t( gen.next() % ( n - 2 ) ); - std::memcpy( needle3, buf.data() + at, 3 ); - } - const std::size_t got3 = sk::find3( buf.data(), n, needle3 ); - const std::size_t ref3 = sk::find3_scalar( buf.data(), n, needle3 ); - std::size_t nai3 = n; - for( std::size_t k = 0; k + 3 <= n; ++k ) - { - if( std::memcmp( buf.data() + k, needle3, 3 ) == 0 ) { nai3 = k; break; } - } - if( findFail.empty() && ( got3 != ref3 || got3 != nai3 ) ) - { - char msg[ 160 ]; - std::snprintf( msg, sizeof( msg ), "find3 iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, got3, ref3, nai3, n ); - findFail = msg; - } - - const std::size_t si = std::size_t( gen.next() % 5u ); - const std::size_t gotS = sk::findByteset( buf.data(), n, *kSets[ si ] ); - const std::size_t refS = sk::findByteset_scalar( buf.data(), n, *kSets[ si ] ); - // the ORACLE re-derives the four words from `bits` through contains(), so this third value is - // what stops the set's two stored representations from drifting apart unseen (2026-09-10). - const std::size_t oraS = sk::findByteset_oracle( buf.data(), n, *kSets[ si ] ); - std::size_t naiS = n; - for( std::size_t k = 0; k < n; ++k ) - { - if( kSets[ si ]->contains( static_cast< unsigned char >( buf[ k ] ) ) ) { naiS = k; break; } - } - if( findFail.empty() && ( gotS != refS || gotS != oraS || gotS != naiS ) ) - { - char msg[ 224 ]; - std::snprintf( msg, sizeof( msg ), "findByteset[%s] iter=%d got=%zu ref=%zu oracle=%zu naive=%zu n=%zu", - kSetNames[ si ], iter, gotS, refS, oraS, naiS, n ); - findFail = msg; - } - } - - // F — tokenizer equivalence on the random corpus - if( tokFail.empty() ) - { - const std::string d = tokenizerDiff( buf ); - if( !d.empty() ) - { - char msg[ 512 ]; - std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d %s", iter, int( alpha ), d.c_str() ); - tokFail = msg; - } - } - } - - checkf( classFail.empty(), "A3 classMasks vs scalar oracle on %zu random buffers (4 alphabets, len 0..300)%s%s", - bufferCount, classFail.empty() ? "" : " — ", classFail.c_str() ); - checkf( foldFail.empty(), "B1 lowerFoldAscii vector == SWAR scalar == the A-Z definition, %zu buffers%s%s", - bufferCount, foldFail.empty() ? "" : " — ", foldFail.c_str() ); - checkf( eqFail.empty(), "C1 lowerFoldedEquals vector == scalar, equal and perturbed, %zu buffers%s%s", - bufferCount, eqFail.empty() ? "" : " — ", eqFail.c_str() ); - checkf( findFail.empty(), "D1/E1 findByte / find3 / findByteset vector == scalar == oracle == naive, %zu buffers%s%s", - bufferCount, findFail.empty() ? "" : " — ", findFail.c_str() ); - checkf( tokFail.empty(), "F2 tokenizer == pre-change walker (spans + fused hashes) on %zu random buffers%s%s", - bufferCount, tokFail.empty() ? "" : " — ", tokFail.c_str() ); - - // ── the real-text corpus ───────────────────────────────────────────────────────────────────────── - std::vector< std::string > files, names; - loadRepoText( root, files, names ); - checkf( files.size() >= 50, "G0 real-text corpus loaded: %zu files under %s/{src,docs} (need >= 50 for the arm to mean anything)", - files.size(), root ); - - std::string realClassFail, realFoldFail, realTokFail, realFindFail; - std::size_t totalBytes = 0; - for( std::size_t fi = 0; fi < files.size(); ++fi ) - { - const std::string& text = files[ fi ]; - totalBytes += text.size(); - - if( realClassFail.empty() ) - { - // every block-aligned window plus the ragged tail — the whole file's bytes are classified - for( std::size_t off = 0; off < text.size() && realClassFail.empty(); off += sk::kBlockBytes ) - { - const std::size_t avail = text.size() - off; - const std::size_t m = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; - sk::Masks got{}, want{}; - sk::classMasks( text.data() + off, m, got ); - sk::classMasks_scalar( text.data() + off, m, want ); - if( !masksEqual( got, want ) ) - { - realClassFail = names[ fi ] + " @" + std::to_string( off ); - } - } - } - if( realFoldFail.empty() ) - { - folded = text; - foldedRef = text; - sk::lowerFoldAscii( folded.data(), folded.size() ); - sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); - if( folded != foldedRef ) - { - realFoldFail = names[ fi ]; - } - else if( !sk::lowerFoldedEquals( text.data(), folded.data(), text.size() ) ) - { - realFoldFail = names[ fi ] + " (foldedEquals)"; - } - } - if( realFindFail.empty() && text.size() >= 3 ) - { - // the needle a --grep trigram probe would use: the file's own middle three bytes - const std::size_t at = text.size() / 2 - 1; - char needle3[ 3 ]; - std::memcpy( needle3, text.data() + at, 3 ); - const std::size_t got3 = sk::find3( text.data(), text.size(), needle3 ); - const std::size_t ref3 = sk::find3_scalar( text.data(), text.size(), needle3 ); - std::size_t nai3 = text.size(); - for( std::size_t k = 0; k + 3 <= text.size(); ++k ) - { - if( std::memcmp( text.data() + k, needle3, 3 ) == 0 ) { nai3 = k; break; } - } - const std::size_t gotS = sk::findByteset( text.data(), text.size(), setXml ); - const std::size_t refS = sk::findByteset_scalar( text.data(), text.size(), setXml ); - const std::size_t oraS = sk::findByteset_oracle( text.data(), text.size(), setXml ); - if( got3 != ref3 || got3 != nai3 || gotS != refS || gotS != oraS ) - { - realFindFail = names[ fi ]; - } - } - if( realTokFail.empty() ) - { - const std::string d = tokenizerDiff( text ); - if( !d.empty() ) - { - realTokFail = names[ fi ] + ": " + d; - } - } - } - - checkf( realClassFail.empty(), "G1 classMasks vs oracle over every byte of src/ + docs/ (%zu files, %zu bytes)%s%s", - files.size(), totalBytes, realClassFail.empty() ? "" : " — ", realClassFail.c_str() ); - checkf( realFoldFail.empty(), "G2 lowerFoldAscii / lowerFoldedEquals over the same %zu files%s%s", - files.size(), realFoldFail.empty() ? "" : " — ", realFoldFail.c_str() ); - checkf( realFindFail.empty(), "G3 find3 / findByteset over the same %zu files%s%s", - files.size(), realFindFail.empty() ? "" : " — ", realFindFail.c_str() ); - checkf( realTokFail.empty(), "G4 tokenizer == pre-change walker over every byte of src/ + docs/ (%zu files, %zu bytes)%s%s", - files.size(), totalBytes, realTokFail.empty() ? "" : " — ", realTokFail.c_str() ); - - std::printf( "%s\n", g_fail == 0 ? "ALL PASS" : "FAILURES ABOVE" ); - return g_fail; -} diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index e33b78783..16a72c843 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -2,25 +2,34 @@ # strkerncheck.sh — SIMD-vs-scalar parity gate for src/infra/strkern.h, and the tokenizer equivalence # gate for the mask-driven walkers in src/lexindex.h. # -# Compiles test/strkern_harness.cpp under the FULL G1 sanitizer set and runs it against (a) 100k -# fixed-seed random buffers over four alphabets, lengths 0..300, and (b) every byte of this repo's src/ -# and docs/. The harness restates each kernel's contract as an independent scalar oracle; the shipped -# vector path (NEON on arm64, AVX2 on x86-64, the scalar twins elsewhere) must match it exactly, and the -# rewritten tokenizer must reproduce the pre-2026-09-10 byte-at-a-time walkers' spans AND fused hashes. +# It drives the CMake target `ripwire_test_strkern` (test/verify_strkern.cpp), the repo's doctest form — +# DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN, one TEST_CASE per kernel, one CHECK/REQUIRE per assertion, built +# beside ripwire_test_csr / ripwire_test_pagerank / ripwire_test_radix. Until 2026-09-10 the same arms +# lived in a standalone test/strkern_harness.cpp (14 `checkf` arms) and test/emitescape_harness.cpp (4); +# the doctest target carries all 18 plus one — a compiled-path assertion — and this gate prints both +# counts so a lost arm is arithmetic, not a feeling. # -# THREE THINGS THIS GATE PROVES, in the order they can go wrong: -# 1 PARITY — vector == scalar == the definition, on random and on real text. -# 2 NON-VACUITY — on arm64 the banner must say NEON, on x86-64 AVX2. A scalar-only build on those -# arches would compare the oracle to itself and pass while proving nothing. -# 3 CAN GO RED — a second build with -DSTRKERN_MUTATE=1 flips one bit of the SIMD-only nibble table, -# narrows the fold's range by one and drops findByteset's high half. That build MUST -# fail. If it passes, the parity assertions above are not binding and this gate is -# decoration. +# THE TARGET IS BUILT THREE TIMES, and each build is a different question: # -# A FOURTH, BEST-EFFORT ARM: on Apple Silicon the AVX2 path is compiled with `-arch x86_64 -# -march=x86-64-v3` and run under Rosetta 2, so the x86 mirror is exercised on this machine rather than -# only on CI's ubuntu legs. It is a SKIP, never a failure, when the SDK or Rosetta is unavailable — the -# authoritative AVX2 proof is the ubuntu-24.04 CI leg. +# 1 CMAKE, FULL G1 SANITIZERS. `cmake -DRIPWIRE_TESTS=ON -DRIPWIRE_ASAN=ON` in a scratch dir, then +# `--target ripwire_test_strkern`. This is the arm that proves the SHIPPED target builds and runs — +# the same target `ctest` runs — under -fsanitize=address,undefined,integer,float-* with +# -fno-sanitize-recover=all, so a nibble-table read one lane past the end ABORTS rather than +# reporting and exiting 0. It runs EVERY test case in the TU (kernels and escapers both): this is +# the G1 arm for the whole file, which is why test/emitescapecheck.sh does not build a second +# sanitized copy of the same source to re-prove it. +# 2 DIRECT $CXX, -DSTRKERN_MUTATE=1. CAN GO RED. The mutation flips one bit of the SIMD-only nibble +# table, narrows the fold's range by one, drops findByteset's high half, and drops the high half of +# the set from Byteset256::words. That build MUST fail. If it passes, every parity assertion above +# is unbinding and this gate is decoration. A compile flag, not a build type — a second CMake +# configure to pass one -D would cost a configure to say nothing extra. +# 3 DIRECT $CXX, `-arch x86_64 -march=x86-64-v3`, run under Rosetta 2. BEST EFFORT. CMake cannot +# express a second architecture for one target inside this tree, so this arm compiles the same +# source the way the pre-doctest gate did. It is a SKIP, never a failure, when the SDK or Rosetta is +# unavailable — the authoritative AVX2 proof is the ubuntu-24.04 CI leg. +# +# NON-VACUITY sits between 1 and 2: on arm64 the banner must say NEON, on x86-64 AVX2. A scalar-only +# build on those arches would compare the oracle to itself and pass while proving nothing. # # Independent of the ripwire binary and of main.cpp (pinned in test/binoverridecheck.sh's EXEMPT dict). # Usage: bash test/strkerncheck.sh (compiles with c++/clang++) @@ -33,54 +42,72 @@ CXX="${CXX:-c++}" # ask THIS front end how it spells C++23 (see scripts/cxxstd.sh — AppleClang 15 rejects -std=c++23) . "$ROOT/scripts/cxxstd.sh" CXXSTD="$( ripwire_cxx_std_flag "$CXX" )" -HARNESS="$ROOT/test/strkern_harness.cpp" +SRC="$ROOT/test/verify_strkern.cpp" WORK="$( mktemp -d )"; trap 'rm -rf "$WORK"' EXIT ARCH="$( uname -m )" fail=0 -echo "strkerncheck: CXX=$CXX arch=$ARCH" +# The arm counts the two standalone harnesses carried before 2026-09-10, kept here so this gate can state +# the before/after rather than assert the after alone. 14 + 4; the doctest target adds the compiled-path +# assertion, so 19 is the floor below. +LEGACY_STRKERN_ARMS=14 +LEGACY_ESCAPE_ARMS=4 +MIN_ASSERTIONS=19 + +echo "strkerncheck: CXX=$CXX arch=$ARCH target=ripwire_test_strkern" + +# ── 1: the CMake target, under the complete G1 sanitizer stack ──────────────────────────────────────── +# FETCHCONTENT_FULLY_DISCONNECTED=ON because every dependency is vendored: a gate must not reach the +# network, and if one ever tries, this is where it fails loudly instead of hanging. +if ! cmake -S "$ROOT" -B "$WORK/cmb" -DRIPWIRE_TESTS=ON -DRIPWIRE_ASAN=ON \ + -DFETCHCONTENT_FULLY_DISCONNECTED=ON > "$WORK/cfg.log" 2>&1; then + echo " FAIL cmake configure (-DRIPWIRE_TESTS=ON -DRIPWIRE_ASAN=ON) failed" + tail -20 "$WORK/cfg.log" | sed 's/^/ /' + exit 2 +fi +if ! cmake --build "$WORK/cmb" --target ripwire_test_strkern -j 2 > "$WORK/build.log" 2>&1; then + echo " FAIL ripwire_test_strkern failed to build under the G1 sanitizers" + tail -30 "$WORK/build.log" | sed 's/^/ /' + exit 2 +fi -# G1's 'integer' / float-cast groups are Clang spellings; GCC only has the address,undefined core. -# Probe THIS front end rather than guessing from its name (same posture as scripts/cxxstd.sh). -SAN="-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow" -printf 'int main(){return 0;}\n' > "$WORK/probe.cpp" 2>/dev/null || true -if ! "$CXX" $SAN -fsyntax-only "$WORK/probe.cpp" 2>/dev/null; then - SAN="-fsanitize=address,undefined" +# Apple's arm64 runtime rejects LeakSanitizer at startup; mirror CMakeLists.txt's platform policy rather +# than claiming a leak check that cannot run (see the note beside ripwire_asan_fixture there). +if [ "$( uname -s )" = "Darwin" ]; then + ASAN_OPTS="detect_leaks=0:halt_on_error=1:abort_on_error=1" +else + ASAN_OPTS="detect_leaks=1:halt_on_error=1:abort_on_error=1" fi +ASAN_OPTIONS="$ASAN_OPTS" UBSAN_OPTIONS="halt_on_error=1:print_stacktrace=1" \ + LSAN_OPTIONS="suppressions=$ROOT/lsan_suppressions.txt" RIPWIRE_ROOT="$ROOT" \ + "$WORK/cmb/ripwire_test_strkern" > "$WORK/out_main.log" 2>&1 +rc=$? -# compile one flavour of the harness; $1 = label, remaining args = extra compile flags. Echoes the binary -# path on success, nothing on failure (the caller decides whether a compile failure is fatal). -compile_harness() +# doctest's own tally line is the arm count: "[doctest] assertions: N | N passed | K failed |" +read_counts() # $1 = log; sets CASES, CASES_PASS, ASSERTS, ASSERTS_FAIL { - local LABEL="$1"; shift - local BIN="$WORK/harness_$LABEL" - if ! "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra "$@" \ - -I"$ROOT/src/infra" -I"$ROOT/src" -I"$ROOT/third_party" \ - "$HARNESS" "$ROOT/src/infra/diagnostics.cpp" -o "$BIN" 2> "$WORK/cc_$LABEL.log"; then - return 1 - fi - printf '%s\n' "$BIN" + CASES="$( sed -n 's/^\[doctest\] test cases: *\([0-9][0-9]*\) .*/\1/p' "$1" | tail -1 )" + CASES_PASS="$( sed -n 's/^\[doctest\] test cases: *[0-9][0-9]* | *\([0-9][0-9]*\) passed.*/\1/p' "$1" | tail -1 )" + ASSERTS="$( sed -n 's/^\[doctest\] assertions: *\([0-9][0-9]*\) .*/\1/p' "$1" | tail -1 )" + ASSERTS_FAIL="$( sed -n 's/.*| *\([0-9][0-9]*\) failed |$/\1/p' "$1" | tail -1 )" + : "${CASES:=0}" "${CASES_PASS:=0}" "${ASSERTS:=0}" "${ASSERTS_FAIL:=0}" } +read_counts "$WORK/out_main.log" -# ── 1 + 2: the shipped path, sanitized, must pass and must not be vacuous ───────────────────────────── -# -fno-sanitize-recover=all is the linchpin: a nibble-table read one lane past the end, or an unaligned -# load the compiler was allowed to assume away, must ABORT rather than report and exit 0. -BIN="$( compile_harness main $SAN -fno-sanitize-recover=all )" -if [ -z "$BIN" ]; then - echo " FAIL harness failed to compile"; sed 's/^/ /' "$WORK/cc_main.log" | head -40; exit 2 -fi - -if ! "$BIN" "$ROOT" > "$WORK/out_main.log" 2>&1; then - echo " FAIL parity/equivalence assertion failed:" - grep -A 2 'FAIL' "$WORK/out_main.log" | sed 's/^/ /' | head -30 +if [ "$rc" -ne 0 ] || [ "${ASSERTS_FAIL:-1}" != "0" ]; then + echo " FAIL parity/equivalence assertion failed (exit $rc, $ASSERTS_FAIL failed):" + grep -B 2 -A 6 'ERROR\|FAILED' "$WORK/out_main.log" | sed 's/^/ /' | head -40 exit 2 fi -if ! grep -q '^ALL PASS$' "$WORK/out_main.log"; then - echo " FAIL harness did not reach its ALL PASS line (truncated run?)" - tail -5 "$WORK/out_main.log" | sed 's/^/ /' +if [ "$ASSERTS" -lt "$MIN_ASSERTIONS" ]; then + echo " FAIL the doctest target ran $ASSERTS assertions; the two harnesses it replaced carried" + echo " $LEGACY_STRKERN_ARMS + $LEGACY_ESCAPE_ARMS = $(( LEGACY_STRKERN_ARMS + LEGACY_ESCAPE_ARMS )), and the target must be >= $MIN_ASSERTIONS." + echo " An arm was deleted, or a TEST_CASE stopped being registered." exit 2 fi -printf ' PASS %s harness arms green (%s)\n' "$( grep -c ' PASS ' "$WORK/out_main.log" )" "$( head -1 "$WORK/out_main.log" | sed 's/strkern: //' )" +printf ' PASS %s test cases / %s assertions green under the full G1 sanitizers (was %s + %s arms in two standalone harnesses) (%s)\n' \ + "$CASES" "$ASSERTS" "$LEGACY_STRKERN_ARMS" "$LEGACY_ESCAPE_ARMS" \ + "$( grep '^strkern: path=' "$WORK/out_main.log" | sed 's/strkern: //' )" WANT="" case "$ARCH" in @@ -89,8 +116,7 @@ case "$ARCH" in esac if [ -n "$WANT" ]; then if grep -q "^strkern path: $WANT$" "$WORK/out_main.log"; then - ok_path="$( grep '^strkern path: ' "$WORK/out_main.log" )" - printf ' PASS non-vacuity: %s on %s\n' "$ok_path" "$ARCH" + printf ' PASS non-vacuity: %s on %s\n' "$( grep '^strkern path: ' "$WORK/out_main.log" )" "$ARCH" else echo " FAIL non-vacuity ($ARCH must compile the $WANT path; banner says '$( grep '^strkern path: ' "$WORK/out_main.log" )')" echo " a scalar-only build here compares the oracle to itself — the parity arms prove nothing" @@ -98,31 +124,49 @@ if [ -n "$WANT" ]; then fi fi -# ── 3: CAN GO RED ───────────────────────────────────────────────────────────────────────────────────── -# The mutation touches ONLY code inside `#if defined( STRKERN_MUTATE )` in the SIMD branches, never the -# scalar oracle — so a red run here is the parity assertion biting, not a broken build. Sanitizers are -# off for this arm: it is expected to fail, and we want it to fail on the assertion, not on a slow abort. -REDBIN="$( compile_harness mutate -DSTRKERN_MUTATE=1 )" +# compile one flavour of the target directly; $1 = label, remaining args = extra compile flags. Echoes the +# binary path on success, nothing on failure (the caller decides whether a compile failure is fatal). +compile_direct() +{ + local LABEL="$1"; shift + local BIN="$WORK/verify_$LABEL" + if ! "$CXX" "$CXXSTD" -O2 -g -Wall -Wextra "$@" \ + -I"$ROOT/src/infra" -I"$ROOT/src" -I"$ROOT/third_party" -I"$ROOT/third_party/deps/doctest" \ + -DRIPWIRE_TEST_ROOT="\"$ROOT\"" \ + "$SRC" "$ROOT/src/infra/diagnostics.cpp" -o "$BIN" 2> "$WORK/cc_$LABEL.log"; then + return 1 + fi + printf '%s\n' "$BIN" +} + +# ── 2: CAN GO RED ───────────────────────────────────────────────────────────────────────────────────── +# The mutation touches ONLY code inside `#if defined( STRKERN_MUTATE )` in src/infra/strkern.h, so a red +# run here is a parity assertion biting, not a broken build. Sanitizers are off for this arm: it is +# expected to fail, and we want it to fail on the assertion, not on a slow abort. +REDBIN="$( compile_direct mutate -DSTRKERN_MUTATE=1 )" if [ -z "$REDBIN" ]; then echo " FAIL can-go-red arm failed to COMPILE (the mutation must build, then fail at runtime)" sed 's/^/ /' "$WORK/cc_mutate.log" | head -20 fail=1 -elif "$REDBIN" "$ROOT" > "$WORK/out_mutate.log" 2>&1; then +elif RIPWIRE_ROOT="$ROOT" "$REDBIN" > "$WORK/out_mutate.log" 2>&1; then echo " FAIL can-go-red: -DSTRKERN_MUTATE=1 build PASSED — the parity assertions are not binding" fail=1 else - printf ' PASS can-go-red: -DSTRKERN_MUTATE=1 fails %s arm(s) as designed\n' "$( grep -c ' FAIL ' "$WORK/out_mutate.log" )" + read_counts "$WORK/out_mutate.log" + printf ' PASS can-go-red: -DSTRKERN_MUTATE=1 fails %s of %s assertions as designed\n' "$ASSERTS_FAIL" "$ASSERTS" fi -# ── 4: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── -# COMMON_RULES for this round: the x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE). -# Compiled without sanitizers — the ASan runtime for a cross-arch slice is not reliably present, and this -# arm's job is to run the AVX2 kernels at all, not to re-prove memory safety the native arm already did. +# ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── +# The x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE; CMakeLists.txt sets it +# unconditionally for x86-64 targets). Compiled without sanitizers — the ASan runtime for a cross-arch +# slice is not reliably present, and this arm's job is to run the AVX2 kernels at all, not to re-prove +# memory safety arm 1 already did. if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then - if X86BIN="$( compile_harness x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then - if "$X86BIN" "$ROOT" > "$WORK/out_x86.log" 2>&1 && grep -q '^ALL PASS$' "$WORK/out_x86.log"; then + if X86BIN="$( compile_direct x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then + if RIPWIRE_ROOT="$ROOT" "$X86BIN" > "$WORK/out_x86.log" 2>&1; then + read_counts "$WORK/out_x86.log" if grep -q '^strkern path: AVX2$' "$WORK/out_x86.log"; then - printf ' PASS x86_64/AVX2 mirror runs green under Rosetta 2 (%s arms)\n' "$( grep -c ' PASS ' "$WORK/out_x86.log" )" + printf ' PASS x86_64/AVX2 mirror runs green under Rosetta 2 (%s assertions)\n' "$ASSERTS" else echo " FAIL x86_64 slice built but did NOT compile the AVX2 path: $( grep '^strkern path: ' "$WORK/out_x86.log" )" fail=1 diff --git a/test/verify_strkern.cpp b/test/verify_strkern.cpp new file mode 100644 index 000000000..07f0f056b --- /dev/null +++ b/test/verify_strkern.cpp @@ -0,0 +1,1280 @@ +// verify_strkern.cpp — the doctest gate for src/infra/strkern.h: SIMD-vs-scalar parity for every kernel, +// the tokenizer-equivalence arm for src/lexindex.h's mask-driven walkers, and the byte-identity arm for +// the three emit escapers the run-copy rewrite touched (rw::escapeXml and rw::appendCdataSafe in +// src/serialize.h, rw::jsonesc::escapeInto in src/infra/jsonesc.h). +// +// It replaces two standalone harnesses — test/strkern_harness.cpp (14 arms) and +// test/emitescape_harness.cpp (4 arms) — with one target in the repo's own doctest form +// (test/verify_csr.cpp, test/verify_pagerank.cpp): DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN, one TEST_CASE per +// kernel and per escaper arm, one CHECK/REQUIRE per assertion, built as the CMake target +// `ripwire_test_strkern` beside `ripwire_test_csr`. Every arm of both harnesses is preserved, including +// both fixed-seed corpora, the all-256-bytes-at-every-offset sweep, and both mutation controls. +// +// A classMasks — per-byte [A-Z]/[a-z]/[0-9]/alnum bitmasks over one block, vector path vs +// the range-test oracle, for EVERY length 0..kBlockBytes. +// B lowerFoldAscii — in-place A-Z fold, vector vs SWAR-scalar, on buffers that straddle every +// block boundary. +// C lowerFoldedEquals — folded compare vs the scalar twin, including every single-byte difference +// position and the case-only differences that are the point of the kernel. +// D findByte / find3 — first-occurrence scans vs the scalar twins AND vs a naive memchr/memcmp +// oracle, needle present and absent, matches at 0 / at n-1 / straddling. +// E findByteset — 256-bit set scan; sets built to hit the (b>>3, b&7) packing's seams +// (empty, full, the 0x80 boundary, one byte only, the XML-escape set) — +// vector == the SHIPPED scalar tail == the re-deriving ORACLE == naive. +// E0 Byteset256 — the set stores TWO derivations of itself (bits for the SIMD table lookups, +// words for the scalar tail's O(1) test), both written by add(). This arm +// reads both back for every set and every byte value; it is the arm that +// exists because the 2026-09-10 tail defect was invisible to all the others. +// F tokenizer equivalence — forEachLexSubtoken / forEachLexSubtokenHashed as shipped vs VERBATIM +// copies of the pre-2026-09-10 byte-at-a-time walkers kept in this file. +// G real text — every arm above re-run over every byte of the repo's src/ and docs/. +// H escapers — the three emit escapers vs the ORIGINAL per-byte loops, frozen verbatim +// here as `*Ref`, over 222k adversarial inputs. +// +// Corpora: (1) a fixed-seed random sweep — 100k buffers, lengths 0..300, drawn from four alphabets +// (identifier-ish, full ASCII, high-bit/UTF-8, and a camel/acronym-dense generator that manufactures the +// exact seams the tokenizer rule turns on); (2) every regular file under src/ and docs/ of the repo root +// (RIPWIRE_ROOT in the environment, else the RIPWIRE_TEST_ROOT this target is compiled with, else "."), +// read whole — the random arms cannot produce the distribution of `ACRONYMWord`, `snake_case` and `//` +// runs the shipped rule was tuned on; (3) the escapers' adversarial corpus, every byte value alone and in +// order, a special byte at every offset of a filler run past two AVX2 blocks, every invalid-UTF-8 shape, +// the CDATA close sequences, and 200k deterministic fuzz strings biased to the special set. +// +// Each corpus is walked ONCE, in a memoised builder, and the TEST_CASEs read its fields — so splitting +// the old bundled arms into one assertion apiece costs no extra pass over 100k buffers. +// +// NON-VACUITY: a TEST_CASE prints the compiled path (`strkern path: NEON|AVX2|scalar`). On arm64/x86_64 +// the gate script REQUIRES a vector path — a scalar-only build there would compare the oracle to itself. +// CAN GO RED: -DSTRKERN_MUTATE=1 perturbs the SIMD tables and drops the high half of the set from the +// Byteset256's `words`; -DEMITESCAPE_MUTATE_BYTESET=1 adds a byteset with '<' missing and asserts the +// comparison SEES it. test/strkerncheck.sh and test/emitescapecheck.sh prove both. + +// READING --quality-delta ON THIS FILE. It reports new-symbol debt here, and three families of it are +// deliberate rather than unfixed. (1) The `*Ref` walkers and escapers are FROZEN VERBATIM copies of the +// code they check — escapeIntoRef ccx=31, refForEachLexSubtoken* ccx=16 — and restructuring an oracle to +// flatter a metric destroys the only thing it is for. (2) The TEST_CASE bodies repeat a 36-43 token +// shape (read the memoised result, INFO the counts, CHECK one field); that repetition IS one-assertion- +// per-test-case, the form this conversion was asked for, and collapsing it would put several arms back +// behind one CHECK. (3) The memoised builders (sweep/realText/escapeRun) are WALKS whose branch count is +// mostly "has this arm already failed" — each per-kernel probe is its own function, which is where the +// splitting was worth doing and where it was done. + +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include + +#include "infra/strkern.h" +#include "infra/jsonesc.h" +#include "lexindex.h" +#include "serialize.h" +#include "harnesscommon.h" // DeterministicRng — the sanitizer-clean generator the SIMD harnesses share + +#include +#include +#include +#include +#include +#include +#include + +#if !defined( RIPWIRE_TEST_ROOT ) + #define RIPWIRE_TEST_ROOT "." +#endif + +namespace sk = rw::strkern; + +using rw::appendCdataSafe; +using rw::escapeXml; +using rw::xmlControlCharRef; +using rw::xmlSafeByte; +using rw::xmlScrubIsLossy; + +namespace +{ + +// The repo root whose src/ and docs/ the real-text arms read. The environment wins so a gate script can +// point the binary at the tree it is checking; the compiled-in source dir keeps a bare `ctest` honest. +const char* repoRoot() +{ + const char* env = std::getenv( "RIPWIRE_ROOT" ); + return ( env != nullptr && env[ 0 ] != '\0' ) ? env : RIPWIRE_TEST_ROOT; +} + +// ============================================================================ +// corpora +// ============================================================================ + +// four alphabets, each aimed at a different failure mode +enum class Alphabet +{ + Identifier, // [A-Za-z0-9_] — the tokenizer's natural food + FullAscii, // 0x00..0x7F — every separator, every nibble-table seam ('@' '[' '`' '{' ':' '/') + HighBit, // 0x00..0xFF — proves the >= 0x80 half is a separator and never folds + CamelDense // manufactured camel / ACRONYMWord / digit seams at high density +}; + +void drawBuffer( DeterministicRng& gen, Alphabet alpha, std::size_t n, std::string& out ) +{ + static const char kIdent[] = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; + out.clear(); + out.reserve( n ); + while( out.size() < n ) + { + const std::uint64_t r = gen.next(); + switch( alpha ) + { + case Alphabet::Identifier: + out.push_back( kIdent[ r % ( sizeof( kIdent ) - 1 ) ] ); + break; + case Alphabet::FullAscii: + out.push_back( char( r & 0x7F ) ); + break; + case Alphabet::HighBit: + out.push_back( char( r & 0xFF ) ); + break; + case Alphabet::CamelDense: + { + // a short run of one class, then switch — so seams land every 1..4 bytes + const int kind = int( r & 3 ); + const std::size_t runLen = 1 + std::size_t( ( r >> 2 ) & 3 ); + for( std::size_t j = 0; j < runLen && out.size() < n; ++j ) + { + const std::uint64_t s = gen.next(); + switch( kind ) + { + case 0: out.push_back( char( 'A' + ( s % 26 ) ) ); break; + case 1: out.push_back( char( 'a' + ( s % 26 ) ) ); break; + case 2: out.push_back( char( '0' + ( s % 10 ) ) ); break; + default: out.push_back( ( s & 1 ) ? '_' : ' ' ); break; + } + } + break; + } + } + } + out.resize( n ); +} + +// every regular file under /src and /docs, read whole +void loadRepoText( const char* root, std::vector& outFiles, std::vector& outNames ) +{ + for( const char* sub : { "src", "docs" } ) + { + const std::filesystem::path dir = std::filesystem::path( root ) / sub; + std::error_code ec; + if( !std::filesystem::is_directory( dir, ec ) ) + { + continue; + } + for( std::filesystem::recursive_directory_iterator it( dir, ec ), end; it != end && !ec; it.increment( ec ) ) + { + if( !it->is_regular_file( ec ) ) + { + continue; + } + std::FILE* fp = std::fopen( it->path().string().c_str(), "rb" ); + if( fp == nullptr ) + { + continue; + } + std::string bytes; + char buf[ 65536 ]; + std::size_t got = 0; + while( ( got = std::fread( buf, 1, sizeof( buf ), fp ) ) > 0 ) + { + bytes.append( buf, got ); + } + std::fclose( fp ); + outFiles.push_back( std::move( bytes ) ); + outNames.push_back( it->path().string() ); + } + } +} + +// ============================================================================ +// the kernels' oracles and comparisons +// ============================================================================ + +bool masksEqual( const sk::Masks& a, const sk::Masks& b ) +{ + return a.alnum == b.alnum && a.upper == b.upper && a.lower == b.lower && a.digit == b.digit; +} + +// ONE window, every length 0..maxN at that offset, vector vs oracle. Both classMasks arms funnel through +// here — the whole-alphabet sweep below and the random sweep's probe — because the compare and the way it +// spells a divergence are the same question asked at two offsets, and two copies of it would be a clone +// the quality delta is right to name. +std::string compareClassMasksWindow( const char* p, std::size_t off, std::size_t maxN ) +{ + for( std::size_t n = 0; n <= maxN; ++n ) + { + sk::Masks got{}, want{}; + sk::classMasks( p + off, n, got ); + sk::classMasks_scalar( p + off, n, want ); + if( !masksEqual( got, want ) ) + { + char msg[ 256 ]; + std::snprintf( msg, sizeof( msg ), + "off=%zu n=%zu got(a=%08x u=%08x l=%08x d=%08x) want(a=%08x u=%08x l=%08x d=%08x)", + off, n, got.alnum, got.upper, got.lower, got.digit, + want.alnum, want.upper, want.lower, want.digit ); + return msg; + } + } + return {}; +} + +// the same, at EVERY offset of one buffer +std::string classMasksSweep( const std::string& text ) +{ + for( std::size_t off = 0; off < text.size(); ++off ) + { + const std::size_t avail = text.size() - off; + const std::string msg = compareClassMasksWindow( text.data(), off, + avail < sk::kBlockBytes ? avail : sk::kBlockBytes ); + if( !msg.empty() ) + { + return msg; + } + } + return {}; +} + +// ============================================================================ +// Arm F — the tokenizer, and the VERBATIM pre-change walkers it must equal +// ============================================================================ + +// Kept byte-for-byte as they stood at 05f4b892 (src/lexindex.h:130 and :201) so this arm compares the new +// mask-driven walkers against the OLD code, not against a paraphrase of it. Do not "clean these up". + +template< class EmitFn > +void refForEachLexSubtoken( std::string_view text, EmitFn&& emit ) +{ + constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); + std::size_t tokStartByte = kNoTokenByte; + bool prevUpper = false; + for( std::size_t k = 0; k < text.size(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( text[ k ] ); + const bool upper = c >= 'A' && c <= 'Z'; + const bool lower = c >= 'a' && c <= 'z'; + const bool digit = c >= '0' && c <= '9'; + if( !upper && !lower && !digit ) + { + if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k ); tokStartByte = kNoTokenByte; } + prevUpper = false; + continue; + } + if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + { + emit( tokStartByte, k ); + tokStartByte = k; + } + if( tokStartByte == kNoTokenByte ) + { + tokStartByte = k; + } + prevUpper = upper; + } + if( tokStartByte != kNoTokenByte ) + { + emit( tokStartByte, text.size() ); + } +} + +template< class EmitFn > +void refForEachLexSubtokenHashed( std::string_view text, EmitFn&& emit ) +{ + constexpr std::size_t kNoTokenByte = ~std::size_t( 0 ); + constexpr std::uint64_t kFnvBasis = 1469598103934665603ull; + std::size_t tokStartByte = kNoTokenByte; + std::uint64_t h = kFnvBasis; + bool prevUpper = false; + const auto mix = [ & ]( unsigned char c ) noexcept { h = rw::hashutil::fnv1aAbsorb( h, char( rw::lexLowerByte( c ) ) ); }; + const auto beginToken = [ & ]( unsigned char c, std::size_t k ) noexcept + { + tokStartByte = k; + h = kFnvBasis; + mix( c ); + }; + for( std::size_t k = 0; k < text.size(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( text[ k ] ); + const bool upper = c >= 'A' && c <= 'Z'; + const bool lower = c >= 'a' && c <= 'z'; + const bool digit = c >= '0' && c <= '9'; + if( !upper && !lower && !digit ) + { + if( tokStartByte != kNoTokenByte ) { emit( tokStartByte, k, h ); tokStartByte = kNoTokenByte; } + prevUpper = false; + continue; + } + if( upper && tokStartByte != kNoTokenByte && rw::lexUpperOpensToken( text, k, prevUpper ) ) + { + emit( tokStartByte, k, h ); + beginToken( c, k ); + prevUpper = true; + continue; + } + if( tokStartByte == kNoTokenByte ) { beginToken( c, k ); prevUpper = upper; continue; } + mix( c ); + prevUpper = upper; + } + if( tokStartByte != kNoTokenByte ) + { + emit( tokStartByte, text.size(), h ); + } +} + +struct Tok +{ + std::size_t start = 0; + std::size_t end = 0; + std::uint64_t hash = 0; +}; + +// Compare all four lists for one text. Returns "" when identical, else the first divergence. +std::string tokenizerDiff( std::string_view text ) +{ + static std::vector< Tok > refH, newH, refS, newS; + refH.clear(); + refForEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) { refH.push_back( { s, e, h } ); } ); + newH.clear(); + rw::forEachLexSubtokenHashed( text, [ & ]( std::size_t s, std::size_t e, std::uint64_t h ) { newH.push_back( { s, e, h } ); } ); + refS.clear(); + refForEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { refS.push_back( { s, e, 0 } ); } ); + newS.clear(); + rw::forEachLexSubtoken( text, [ & ]( std::size_t s, std::size_t e ) { newS.push_back( { s, e, 0 } ); } ); + + char msg[ 384 ]; + if( refS.size() != newS.size() ) + { + std::snprintf( msg, sizeof( msg ), "span COUNT %zu vs %zu (len=%zu)", refS.size(), newS.size(), text.size() ); + return msg; + } + for( std::size_t i = 0; i < refS.size(); ++i ) + { + if( refS[ i ].start != newS[ i ].start || refS[ i ].end != newS[ i ].end ) + { + std::snprintf( msg, sizeof( msg ), "span #%zu [%zu,%zu) vs [%zu,%zu) (len=%zu)", i, + refS[ i ].start, refS[ i ].end, newS[ i ].start, newS[ i ].end, text.size() ); + return msg; + } + } + if( refH.size() != newH.size() ) + { + std::snprintf( msg, sizeof( msg ), "hashed COUNT %zu vs %zu (len=%zu)", refH.size(), newH.size(), text.size() ); + return msg; + } + for( std::size_t i = 0; i < refH.size(); ++i ) + { + if( refH[ i ].start != newH[ i ].start || refH[ i ].end != newH[ i ].end || refH[ i ].hash != newH[ i ].hash ) + { + std::snprintf( msg, sizeof( msg ), "hashed #%zu [%zu,%zu)#%016llx vs [%zu,%zu)#%016llx (len=%zu)", i, + refH[ i ].start, refH[ i ].end, ( unsigned long long )refH[ i ].hash, + newH[ i ].start, newH[ i ].end, ( unsigned long long )newH[ i ].hash, text.size() ); + return msg; + } + // and the fused hash must still equal the standalone lexSubtokenHash of the same span + const std::uint64_t standalone = rw::lexSubtokenHash( text.data() + newH[ i ].start, newH[ i ].end - newH[ i ].start ); + if( standalone != newH[ i ].hash ) + { + std::snprintf( msg, sizeof( msg ), "fused hash #%zu %016llx != lexSubtokenHash %016llx", i, + ( unsigned long long )newH[ i ].hash, ( unsigned long long )standalone ); + return msg; + } + // ... and the hash-free walker's spans must be the same spans + if( refS[ i ].start != newH[ i ].start || refS[ i ].end != newH[ i ].end ) + { + std::snprintf( msg, sizeof( msg ), "walker disagreement #%zu [%zu,%zu) vs [%zu,%zu)", i, + refS[ i ].start, refS[ i ].end, newH[ i ].start, newH[ i ].end ); + return msg; + } + } + return {}; +} + +// ============================================================================ +// the memoised sweeps — each corpus is walked ONCE, whatever the test order +// ============================================================================ + +struct Sets +{ + sk::Byteset256 setEmpty, setFull, setOne, setHighOnly, setXml; + const sk::Byteset256* all[ 5 ] = {}; + const char* names[ 5 ] = { "empty", "full", "one", "high", "xml" }; + + Sets() + { + for( unsigned b = 0; b < 256u; ++b ) + { + setFull.add( static_cast< unsigned char >( b ) ); + } + setOne.add( 'q' ); + setHighOnly.addRange( 0x80, 0xFF ); + for( char c : { '&', '<', '>', '"', '\'', '\t', '\n', '\r' } ) + { + setXml.add( static_cast< unsigned char >( c ) ); + } + setXml.addRange( 0x80, 0xFF ); + all[ 0 ] = &setEmpty; all[ 1 ] = &setFull; all[ 2 ] = &setOne; all[ 3 ] = &setHighOnly; all[ 4 ] = &setXml; + } +}; + +const Sets& sets() +{ + static const Sets s; + return s; +} + +struct Sweep +{ + std::size_t bufferCount = 0; + std::string classFail, foldFail, eqFail, findFail, tokFail; +}; + +// ── one probe per kernel ────────────────────────────────────────────────────────────────────────────── +// The sweep below is a WALK, and each of these is one kernel's question asked of the buffer it is +// standing on. They take `gen` rather than pre-drawn values so the draw ORDER — and therefore the corpus +// every arm sees — is exactly the one the standalone harness drew before 2026-09-10. Each returns "" or +// the first divergence, spelled so a failure names the input rather than an offset. + +// A — classMasks at every offset/length that fits in one block, but only on a slice (the full +// O(n * block) sweep on 100k buffers would dominate the gate's runtime). +std::string probeClassMasks( DeterministicRng& gen, const std::string& buf, int iter, Alphabet alpha ) +{ + const std::size_t n = buf.size(); + const std::size_t probeOff = n == 0 ? 0 : std::size_t( gen.next() % n ); + const std::size_t avail = n - probeOff; + const std::string msg = compareClassMasksWindow( buf.data(), probeOff, + avail < sk::kBlockBytes ? avail : sk::kBlockBytes ); + if( msg.empty() ) + { + return {}; + } + char out[ 320 ]; + std::snprintf( out, sizeof( out ), "iter=%d alpha=%d %s", iter, int( alpha ), msg.c_str() ); + return out; +} + +// B — lowerFoldAscii, vector vs SWAR scalar, in place, and then against the A-Z definition byte by byte. +std::string probeFold( const std::string& buf, int iter, Alphabet alpha ) +{ + std::string folded( buf ), foldedRef( buf ); + sk::lowerFoldAscii( folded.data(), folded.size() ); + sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); + if( folded != foldedRef ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d n=%zu", iter, int( alpha ), buf.size() ); + return msg; + } + for( std::size_t k = 0; k < buf.size(); ++k ) + { + const unsigned char c = static_cast< unsigned char >( buf[ k ] ); + const unsigned char want = ( c >= 'A' && c <= 'Z' ) ? static_cast< unsigned char >( c + 0x20 ) : c; + if( static_cast< unsigned char >( folded[ k ] ) != want ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "definition iter=%d k=%zu byte=%02x", iter, k, c ); + return msg; + } + } + return {}; +} + +// C — lowerFoldedEquals: the equal case, then every single-byte perturbation of one random position. +std::string probeFoldedEquals( DeterministicRng& gen, const std::string& buf, int iter ) +{ + const std::size_t n = buf.size(); + std::string lowered( buf ); + sk::lowerFoldAscii_scalar( lowered.data(), lowered.size() ); + if( !sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) + || !sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) + { + return "self-compare returned false"; + } + const std::size_t at = std::size_t( gen.next() % n ); + const char old = lowered[ at ]; + lowered[ at ] = char( static_cast< unsigned char >( old ) ^ 0x01 ); + if( sk::lowerFoldedEquals( buf.data(), lowered.data(), n ) != sk::lowerFoldedEquals_scalar( buf.data(), lowered.data(), n ) ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "perturbed iter=%d at=%zu n=%zu", iter, at, n ); + return msg; + } + return {}; +} + +// D — findByte, vector vs scalar twin vs a naive memchr oracle. +std::string probeFindByte( DeterministicRng& gen, const std::string& buf, int iter ) +{ + const std::size_t n = buf.size(); + const char needle = char( gen.next() & 0xFF ); + const std::size_t got = sk::findByte( buf.data(), n, needle ); + const std::size_t ref = sk::findByte_scalar( buf.data(), n, needle ); + std::size_t naive = n; + for( std::size_t k = 0; k < n; ++k ) + { + if( buf[ k ] == needle ) { naive = k; break; } + } + if( got != ref || got != naive ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "findByte iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, got, ref, naive, n ); + return msg; + } + return {}; +} + +// D — find3. Half the time the needle is PLANTED so a hit is exercised, half the time drawn at random. +std::string probeFind3( DeterministicRng& gen, const std::string& buf, int iter ) +{ + const std::size_t n = buf.size(); + char needle3[ 3 ] = { char( gen.next() & 0xFF ), char( gen.next() & 0xFF ), char( gen.next() & 0xFF ) }; + if( n >= 3 && ( gen.next() & 1 ) ) + { + const std::size_t at = std::size_t( gen.next() % ( n - 2 ) ); + std::memcpy( needle3, buf.data() + at, 3 ); + } + const std::size_t got = sk::find3( buf.data(), n, needle3 ); + const std::size_t ref = sk::find3_scalar( buf.data(), n, needle3 ); + std::size_t naive = n; + for( std::size_t k = 0; k + 3 <= n; ++k ) + { + if( std::memcmp( buf.data() + k, needle3, 3 ) == 0 ) { naive = k; break; } + } + if( got != ref || got != naive ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "find3 iter=%d got=%zu ref=%zu naive=%zu n=%zu", iter, got, ref, naive, n ); + return msg; + } + return {}; +} + +// E — findByteset: vector == the SHIPPED scalar tail == the re-deriving ORACLE == naive. The oracle is +// the third value on purpose — it rebuilds the four words from `bits` through contains(), so it is what +// stops the set's two stored representations from drifting apart unseen. +std::string probeFindByteset( DeterministicRng& gen, const std::string& buf, int iter ) +{ + const Sets& S = sets(); + const std::size_t n = buf.size(); + const std::size_t si = std::size_t( gen.next() % 5u ); + const std::size_t got = sk::findByteset( buf.data(), n, *S.all[ si ] ); + const std::size_t ref = sk::findByteset_scalar( buf.data(), n, *S.all[ si ] ); + const std::size_t ora = sk::findByteset_oracle( buf.data(), n, *S.all[ si ] ); + std::size_t naive = n; + for( std::size_t k = 0; k < n; ++k ) + { + if( S.all[ si ]->contains( static_cast< unsigned char >( buf[ k ] ) ) ) { naive = k; break; } + } + if( got != ref || got != ora || got != naive ) + { + char msg[ 224 ]; + std::snprintf( msg, sizeof( msg ), "findByteset[%s] iter=%d got=%zu ref=%zu oracle=%zu naive=%zu n=%zu", + S.names[ si ], iter, got, ref, ora, naive, n ); + return msg; + } + return {}; +} + +const Sweep& sweep() +{ + static const Sweep s = [] + { + Sweep r; + DeterministicRng gen{ 0x5DEECE66Dull }; + std::string buf; + + for( int iter = 0; iter < 100000; ++iter ) + { + const Alphabet alpha = Alphabet( iter & 3 ); + const std::size_t n = std::size_t( gen.next() % 301u ); // 0..300, straddles 16/32 repeatedly + drawBuffer( gen, alpha, n, buf ); + ++r.bufferCount; + + // Each probe is skipped once its arm has already failed — the arms report the FIRST + // divergence, and a kernel that is broken is broken 100k times over. + if( r.classFail.empty() ) { r.classFail = probeClassMasks( gen, buf, iter, alpha ); } + if( r.foldFail.empty() ) { r.foldFail = probeFold( buf, iter, alpha ); } + if( r.eqFail.empty() && n > 0 ) { r.eqFail = probeFoldedEquals( gen, buf, iter ); } + if( r.findFail.empty() ) { r.findFail = probeFindByte( gen, buf, iter ); } + if( r.findFail.empty() ) { r.findFail = probeFind3( gen, buf, iter ); } + if( r.findFail.empty() ) { r.findFail = probeFindByteset( gen, buf, iter ); } + if( r.tokFail.empty() ) + { + const std::string d = tokenizerDiff( buf ); + if( !d.empty() ) + { + char msg[ 512 ]; + std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d %s", iter, int( alpha ), d.c_str() ); + r.tokFail = msg; + } + } + } + return r; + }(); + return s; +} + +struct RealText +{ + std::size_t fileCount = 0; + std::size_t totalBytes = 0; + std::string classFail, foldFail, findFail, tokFail; +}; + +// ── the same questions, asked of one real file ──────────────────────────────────────────────────────── +// Real text is not optional here: the random alphabets cannot produce the distribution of `ACRONYMWord`, +// `snake_case` and `//` runs the shipped tokenizer rule was tuned on, nor the doc-comment shapes the +// escapers meet. Each returns "" or the name of the file that diverged. + +// every block-aligned window plus the ragged tail — the whole file's bytes are classified +std::string probeFileClassMasks( const std::string& text, const std::string& name ) +{ + for( std::size_t off = 0; off < text.size(); off += sk::kBlockBytes ) + { + const std::size_t avail = text.size() - off; + const std::size_t m = avail < sk::kBlockBytes ? avail : sk::kBlockBytes; + sk::Masks got{}, want{}; + sk::classMasks( text.data() + off, m, got ); + sk::classMasks_scalar( text.data() + off, m, want ); + if( !masksEqual( got, want ) ) + { + return name + " @" + std::to_string( off ); + } + } + return {}; +} + +std::string probeFileFold( const std::string& text, const std::string& name ) +{ + std::string folded( text ), foldedRef( text ); + sk::lowerFoldAscii( folded.data(), folded.size() ); + sk::lowerFoldAscii_scalar( foldedRef.data(), foldedRef.size() ); + if( folded != foldedRef ) + { + return name; + } + if( !sk::lowerFoldedEquals( text.data(), folded.data(), text.size() ) ) + { + return name + " (foldedEquals)"; + } + return {}; +} + +// the needle a --grep trigram probe would use: the file's own middle three bytes +std::string probeFileFinds( const std::string& text, const std::string& name ) +{ + if( text.size() < 3 ) + { + return {}; + } + const Sets& S = sets(); + const std::size_t at = text.size() / 2 - 1; + char needle3[ 3 ]; + std::memcpy( needle3, text.data() + at, 3 ); + const std::size_t got3 = sk::find3( text.data(), text.size(), needle3 ); + const std::size_t ref3 = sk::find3_scalar( text.data(), text.size(), needle3 ); + std::size_t naive = text.size(); + for( std::size_t k = 0; k + 3 <= text.size(); ++k ) + { + if( std::memcmp( text.data() + k, needle3, 3 ) == 0 ) { naive = k; break; } + } + const std::size_t gotS = sk::findByteset( text.data(), text.size(), S.setXml ); + const std::size_t refS = sk::findByteset_scalar( text.data(), text.size(), S.setXml ); + const std::size_t oraS = sk::findByteset_oracle( text.data(), text.size(), S.setXml ); + if( got3 != ref3 || got3 != naive || gotS != refS || gotS != oraS ) + { + return name; + } + return {}; +} + +const RealText& realText() +{ + static const RealText s = [] + { + RealText r; + std::vector< std::string > files, names; + loadRepoText( repoRoot(), files, names ); + r.fileCount = files.size(); + + for( std::size_t fi = 0; fi < files.size(); ++fi ) + { + const std::string& text = files[ fi ]; + r.totalBytes += text.size(); + if( r.classFail.empty() ) { r.classFail = probeFileClassMasks( text, names[ fi ] ); } + if( r.foldFail.empty() ) { r.foldFail = probeFileFold( text, names[ fi ] ); } + if( r.findFail.empty() ) { r.findFail = probeFileFinds( text, names[ fi ] ); } + if( r.tokFail.empty() ) + { + const std::string d = tokenizerDiff( text ); + if( !d.empty() ) + { + r.tokFail = names[ fi ] + ": " + d; + } + } + } + return r; + }(); + return s; +} + +// ============================================================================ +// Arm H — the emit escapers and the frozen per-byte references they must equal +// ============================================================================ + +std::string escapeXmlRef( std::string_view s ) +{ + std::string out; + const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; + const char* d = s.data(); + const std::size_t n = s.size(); + for( std::size_t i = 0; i < n; ) + { + const char c = d[i]; + switch( c ) + { + case '&': put( "&" ); ++i; break; + case '<': put( "<" ); ++i; break; + case '>': put( ">" ); ++i; break; + case '"': put( """ ); ++i; break; + case '\'': put( "'" ); ++i; break; + case '\t': + case '\n': + case '\r': put( xmlControlCharRef( c ) ); ++i; break; + default: + if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } + else if( const int len = rw::jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } + else + { + for( int k = 0; k < len; ++k ) + { + out.push_back( d[i + k] ); + } + i += std::size_t( len ); + } + } + } + return out; +} + +std::string appendCdataSafeRef( std::string_view body ) +{ + std::string safe; + const char* d = body.data(); + const std::size_t n = body.size(); + for( std::size_t i = 0; i < n; ) + { + if( i + 2 < n && d[i] == ']' && d[i + 1] == ']' && d[i + 2] == '>' ) + { safe += "]]]]>"; i += 3; continue; } + const unsigned char c = static_cast( d[i] ); + if( c < 0x80 ) { safe += xmlSafeByte( d[i] ); ++i; } + else if( const int len = rw::jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { safe += '?'; ++i; } + else { safe.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } + } + return safe; +} + +std::string escapeIntoRef( std::string_view s, bool escapeAngleAmp, bool validateUtf8, bool replacementAsTextEscape ) +{ + std::string out; + const char* d = s.data(); + const std::size_t n = s.size(); + std::size_t i = 0; + while( i < n ) + { + const unsigned char c = static_cast( d[i] ); + if( c < 0x80 ) + { + switch( c ) + { + case '"': out += "\\\""; ++i; continue; + case '\\': out += "\\\\"; ++i; continue; + case '\n': out += "\\n"; ++i; continue; + case '\r': out += "\\r"; ++i; continue; + case '\t': out += "\\t"; ++i; continue; + case '<': if( escapeAngleAmp ) { out += "\\u003c"; ++i; continue; } break; + case '>': if( escapeAngleAmp ) { out += "\\u003e"; ++i; continue; } break; + case '&': if( escapeAngleAmp ) { out += "\\u0026"; ++i; continue; } break; + default: break; + } + if( c < 0x20 ) + { char b[ 8 ]; std::snprintf( b, sizeof( b ), "\\u%04x", unsigned( c ) ); out += b; } + else + { + out += char( c ); + } + ++i; + continue; + } + if( !validateUtf8 ) { out += char( c ); ++i; continue; } + const int len = rw::jsonesc::utf8SeqLen( d, i, n ); + if( len == 0 ) + { + if( replacementAsTextEscape ) { out += "\\ufffd"; } + else { out += "\xEF\xBF\xBD"; } + ++i; + } + else { out.append( d + i, std::size_t( len ) ); i += std::size_t( len ); } + } + return out; +} + +// ── the shipped functions, wrapped to the same signature ────────────────────────────────────────────── + +std::string escapeXmlNew( std::string_view s ) +{ + std::vector buf; + const std::string_view v = escapeXml( s, buf ); + return std::string( v ); +} + +std::string appendCdataSafeNew( std::string_view s ) +{ + std::string out; + appendCdataSafe( s, out ); + return out; +} + +std::string escapeIntoNew( std::string_view s, bool a, bool v, bool r ) +{ + std::string out; + rw::jsonesc::escapeInto( s, out, a, v, r ); + return out; +} + +// ── MUT: the same run-copy shape with '<' dropped from the byte set ─────────────────────────────────── +// Deliberately WRONG. Not compiled into anything shipped; it exists so the gate can prove that this +// comparison actually notices a set member going missing (a byteset bug is silent otherwise — the output +// is still well-formed-looking text, just with a raw '<' where an entity belonged). +#if EMITESCAPE_MUTATE_BYTESET +std::string escapeXmlMutatedSet( std::string_view s ) +{ + std::string out; + const char* d = s.data(); + const std::size_t n = s.size(); + const auto put = [ & ]( const char* lit ) { while( *lit ) { out.push_back( *lit++ ); } }; + for( std::size_t i = 0; i < n; ) + { + const char c = d[i]; + switch( c ) + { + // '<' intentionally absent from the set — falls through to the verbatim copy below. + case '&': put( "&" ); ++i; break; + case '>': put( ">" ); ++i; break; + case '"': put( """ ); ++i; break; + case '\'': put( "'" ); ++i; break; + case '\t': + case '\n': + case '\r': put( xmlControlCharRef( c ) ); ++i; break; + default: + if( static_cast( c ) < 0x80 ) { out.push_back( xmlSafeByte( c ) ); ++i; } + else if( const int len = rw::jsonesc::utf8SeqLen( d, i, n ); len == 0 ) { out.push_back( '?' ); ++i; } + else + { + for( int k = 0; k < len; ++k ) { out.push_back( d[i + k] ); } + i += std::size_t( len ); + } + } + } + return out; +} +#endif + +// ── the adversarial corpus, one function per shape it is adversarial about ──────────────────────────── + +void addCase( std::vector& v, std::string s ) { v.push_back( std::move( s ) ); } + +// A — every byte value alone, and all 256 in order. +void addByteValueCases( std::vector& cases ) +{ + std::string all; + for( int b = 0; b < 256; ++b ) + { + addCase( cases, std::string( 1, char( b ) ) ); + all.push_back( char( b ) ); + } + addCase( cases, all ); + addCase( cases, std::string() ); +} + +// B — a special byte planted at every offset of a filler run, across every length up to two 32-byte AVX2 +// blocks plus a tail: the block-boundary sweep a SIMD run loop and its scalar tail must both survive. +void addOffsetSweepCases( std::vector& cases ) +{ + const char specials[] = { '&', '<', '>', '"', '\'', '\t', '\n', '\r', '\0', '\x0b', '\x1f', '\x7f', + char( 0x80 ), char( 0xC3 ), char( 0xFF ), ']' }; + for( char sp : specials ) + { + for( std::size_t len = 1; len <= 96; ++len ) + { + for( std::size_t at = 0; at < len; at += ( len > 40 ? 7 : 1 ) ) + { + std::string s( len, 'a' ); + s[at] = sp; + addCase( cases, s ); + } + } + } +} + +// C/D — invalid UTF-8 shapes (bare continuation, overlong 2/3/4-byte forms, surrogate halves, >U+10FFFF, +// a sequence truncated at end-of-buffer) and the valid multibyte + BOM cases they must not be confused +// with. Each is placed alone, around specials, and at 31/32/33 bytes so a block boundary splits it. +void addUtf8Cases( std::vector& cases ) +{ + const char* bad[] = { + "\x80", "\xBF", "\xC0\x80", "\xC1\xBF", "\xC2", "\xE0\x80\x80", "\xE0\x9F\xBF", + "\xED\xA0\x80", "\xED\xBF\xBF", "\xE2\x82", "\xF0\x80\x80\x80", "\xF0\x8F\xBF\xBF", + "\xF4\x90\x80\x80", "\xF5\x80\x80\x80", "\xFE", "\xFF", "\xF0\x9D\x84", + }; + for( const char* b : bad ) + { + std::string s( b ); + addCase( cases, s ); + addCase( cases, "abc" + s ); + addCase( cases, s + "abc" ); + addCase( cases, "abc" + s + "<&>" ); + addCase( cases, std::string( 31, 'x' ) + s ); + addCase( cases, std::string( 32, 'x' ) + s ); + addCase( cases, std::string( 33, 'x' ) + s ); + } + // lone continuation byte as the very last byte of the buffer + addCase( cases, std::string( 40, 'q' ) + "\xBF" ); + addCase( cases, std::string( 40, 'q' ) + "\xE2\x82" ); + + const char* good[] = { "\xC3\xA9", "\xE2\x82\xAC", "\xF0\x9D\x84\x9E", "\xEF\xBB\xBF", "\xEF\xBF\xBD" }; + for( const char* g : good ) + { + std::string s( g ); + addCase( cases, s ); + addCase( cases, s + "<" + s ); + addCase( cases, std::string( 30, 'z' ) + s + std::string( 30, 'z' ) ); + addCase( cases, std::string( 31, 'z' ) + s ); + } +} + +// E — the CDATA close sequences appendCdataSafe splits, including the ones that only LOOK like one. +void addCdataCases( std::vector& cases ) +{ + addCase( cases, "]]>" ); + addCase( cases, "]]" ); + addCase( cases, "]" ); + addCase( cases, "]]]" ); + addCase( cases, "]]]]>" ); + addCase( cases, "a]]>b" ); + addCase( cases, "]]>]]>" ); + addCase( cases, std::string( 31, 'p' ) + "]]>" ); + addCase( cases, std::string( 32, 'p' ) + "]]>" + std::string( 32, 'p' ) ); + addCase( cases, std::string( 30, 'p' ) + "]]" ); + addCase( cases, "]]\x01>" ); +} + +// G — 200k deterministic fuzz strings over an alphabet biased to the special set. Fixed seed, so a +// failure reproduces anywhere. +void addFuzzCases( std::vector& cases ) +{ + DeterministicRng rng{ 0x9E3779B97F4A7C15ull }; + const std::string alphabet = "abcdefgh<>&\"'\t\n\r]] \x01\x1f\x7f\x80\xC3\xA9\xE2\x82\xAC\xF0\x9D\x84\x9E\xFF"; + for( int k = 0; k < 200000; ++k ) + { + const std::size_t len = std::size_t( rng.next() % 201 ); + std::string s; + s.reserve( len ); + for( std::size_t j = 0; j < len; ++j ) + { + s.push_back( alphabet[ std::size_t( rng.next() % alphabet.size() ) ] ); + } + cases.push_back( std::move( s ) ); + } +} + +std::vector buildEscapeCorpus() +{ + std::vector cases; + addByteValueCases( cases ); + addOffsetSweepCases( cases ); + addUtf8Cases( cases ); + addCdataCases( cases ); + addFuzzCases( cases ); + return cases; +} + +// appendCdataSafe's only non-scrub rewrite is the "]]>" split, so on an input xmlScrubIsLossy calls +// CLEAN the escaped form must equal the input with that one substitution applied and nothing else. This +// is the cheap direction of the §B12.7 disclosure predicate, and it is the direction a run-copy bug in +// the escaper would break: a skipped run is a moved byte. +bool lossyDisagrees( const std::string& s ) +{ + if( xmlScrubIsLossy( s ) ) + { + return false; + } + std::string expanded; + for( std::size_t i = 0; i < s.size(); ) + { + if( i + 2 < s.size() && s[i] == ']' && s[i + 1] == ']' && s[i + 2] == '>' ) + { expanded += "]]]]>"; i += 3; } + else { expanded += s[i]; ++i; } + } + return appendCdataSafeRef( s ) != expanded; +} + +struct EscapeRun +{ + std::size_t caseCount = 0; + std::size_t xmlBad = 0, cdataBad = 0, jsonBad = 0, lossyBad = 0, mutDiff = 0; +}; + +const EscapeRun& escapeRun() +{ + static const EscapeRun s = [] + { + EscapeRun r; + const std::vector cases = buildEscapeCorpus(); + r.caseCount = cases.size(); + for( const std::string& s : cases ) + { + if( escapeXmlNew( s ) != escapeXmlRef( s ) ) { ++r.xmlBad; } + if( appendCdataSafeNew( s ) != appendCdataSafeRef( s ) ) { ++r.cdataBad; } + for( int mode = 0; mode < 8; ++mode ) + { + const bool a = ( mode & 1 ) != 0; + const bool v = ( mode & 2 ) != 0; + const bool t = ( mode & 4 ) != 0; + if( escapeIntoNew( s, a, v, t ) != escapeIntoRef( s, a, v, t ) ) { ++r.jsonBad; } + } + if( lossyDisagrees( s ) ) { ++r.lossyBad; } +#if EMITESCAPE_MUTATE_BYTESET + if( escapeXmlMutatedSet( s ) != escapeXmlRef( s ) ) { ++r.mutDiff; } +#endif + } + return r; + }(); + return s; +} + +} // namespace + +// ============================================================================ +// the arms +// ============================================================================ + +TEST_CASE( "strkern: the compiled path is the one this target claims" ) +{ + // NON-VACUITY BANNER. printf, not doctest's MESSAGE, because test/strkerncheck.sh greps this line + // exactly: on arm64 it must say NEON and on x86-64 AVX2, or the parity arms below compare the scalar + // oracle to itself and prove nothing. + std::printf( "strkern: path=%s block=%zu root=%s\n", sk::kPathName, sk::kBlockBytes, repoRoot() ); + std::printf( "strkern path: %s\n", sk::kPathName ); + CHECK( sk::kBlockBytes <= sk::kMaxBlockBytes ); +} + +TEST_CASE( "strkern: A1 classMasks over all 256 byte values, every offset and length" ) +{ + std::string every; + for( unsigned b = 0; b < 256u; ++b ) + { + every.push_back( char( b ) ); + } + const std::string fail = classMasksSweep( every ); + INFO( "first divergence: ", fail ); + CHECK( fail.empty() ); +} + +TEST_CASE( "strkern: A2 classMasks single-byte classes are exactly [A-Z]/[a-z]/[0-9]" ) +{ + // bytes >= 0x80 are separators by construction — the high-nibble table maps 8..F to 0 + bool defOk = true; + for( unsigned b = 0; b < 256u; ++b ) + { + const char c = char( b ); + sk::Masks m{}; + sk::classMasks( &c, 1, m ); + const bool wantUpper = b >= 'A' && b <= 'Z'; + const bool wantLower = b >= 'a' && b <= 'z'; + const bool wantDigit = b >= '0' && b <= '9'; + defOk = defOk && ( ( m.upper & 1u ) != 0 ) == wantUpper && ( ( m.lower & 1u ) != 0 ) == wantLower + && ( ( m.digit & 1u ) != 0 ) == wantDigit + && ( ( m.alnum & 1u ) != 0 ) == ( wantUpper || wantLower || wantDigit ); + } + CHECK( defOk ); +} + +TEST_CASE( "strkern: A3 classMasks vector == scalar oracle over the random corpus" ) +{ + const Sweep& s = sweep(); + INFO( "buffers: ", s.bufferCount, " first divergence: ", s.classFail ); + CHECK( s.classFail.empty() ); +} + +TEST_CASE( "strkern: B1 lowerFoldAscii vector == SWAR scalar == the A-Z definition" ) +{ + const Sweep& s = sweep(); + INFO( "buffers: ", s.bufferCount, " first divergence: ", s.foldFail ); + CHECK( s.foldFail.empty() ); +} + +TEST_CASE( "strkern: C1 lowerFoldedEquals vector == scalar, equal and perturbed" ) +{ + const Sweep& s = sweep(); + INFO( "buffers: ", s.bufferCount, " first divergence: ", s.eqFail ); + CHECK( s.eqFail.empty() ); +} + +TEST_CASE( "strkern: D1/E1 findByte / find3 / findByteset vector == scalar == oracle == naive" ) +{ + const Sweep& s = sweep(); + INFO( "buffers: ", s.bufferCount, " first divergence: ", s.findFail ); + CHECK( s.findFail.empty() ); +} + +TEST_CASE( "strkern: E0 Byteset256 carries two agreeing representations (bits vs words)" ) +{ + // The set stores the SIMD tables' (b >> 3, b & 7) packing and the scalar tail's (b >> 6, b & 63) words, + // both written by add(). Nothing else in the header would notice one of them going stale — and the + // 2026-09-10 defect this arm was written for (the tail re-deriving its words on every call) was + // invisible to every correctness arm in this file. + const Sets& S = sets(); + std::string fail; + for( std::size_t si = 0; si < 5 && fail.empty(); ++si ) + { + std::uint64_t rederived[ 4 ] = { 0, 0, 0, 0 }; + for( unsigned b = 0; b < 256u && fail.empty(); ++b ) + { + const unsigned char c = static_cast< unsigned char >( b ); + if( S.all[ si ]->contains( c ) ) + { + rederived[ b >> 6 ] |= std::uint64_t( 1 ) << ( b & 63u ); + } + if( S.all[ si ]->contains( c ) != S.all[ si ]->containsWord( c ) ) + { + char msg[ 128 ]; + std::snprintf( msg, sizeof( msg ), "set=%s byte=%02x bits=%d words=%d", S.names[ si ], b, + int( S.all[ si ]->contains( c ) ), int( S.all[ si ]->containsWord( c ) ) ); + fail = msg; + } + } + for( int w = 0; w < 4 && fail.empty(); ++w ) + { + if( rederived[ w ] != S.all[ si ]->words[ w ] ) + { + char msg[ 160 ]; + std::snprintf( msg, sizeof( msg ), "set=%s word[%d] stored=%016llx rederived=%016llx", + S.names[ si ], w, ( unsigned long long )S.all[ si ]->words[ w ], + ( unsigned long long )rederived[ w ] ); + fail = msg; + } + } + } + INFO( "first divergence: ", fail ); + CHECK( fail.empty() ); +} + +TEST_CASE( "strkern: F1 tokenizer equals the pre-change walker on the registered seam cases" ) +{ + // The hand-written seam table from docs/EVALS.md §4 — the cases the acronym rule exists for, spelled + // out so a failure names the input rather than a random offset. + static const char* kCases[] = { + "", "a", "A", "aB", "Ab", "AB", "ABc", "aBc", "MCP", "MCP2Server", "HTTPServer", "IOError", + "XMLHttpRequest", "_max_speed", "updateCollisionPositionVelocity", "foo bar", " ", "__", + "A1B2C3", "camelCASE", "CASEcamel", "endsWithUPPER", "x", "0", "9a", "a9", "Z", "aZ", "ZZa", + "ZZZZZZZZZZZZZZZZZZZZa", // acronym run straddling a 16-byte block + "aaaaaaaaaaaaaaaBcccccccccccccccDeeeeeeeeeeeeeeeF", // camel seam at 15/31/47 + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaBc", // camel seam exactly at 32 + "ABCDEFGHIJKLMNOPa", "ABCDEFGHIJKLMNOPQRSTUVWXYZABCDEFa", // acronym seam at 16 and at 32 + "____________________abc", "abc____________________", + }; + std::string firstFail; + for( const char* c : kCases ) + { + const std::string d = tokenizerDiff( c ); + if( !d.empty() && firstFail.empty() ) + { + firstFail = std::string( "\"" ) + c + "\": " + d; + } + } + INFO( "cases: ", sizeof( kCases ) / sizeof( kCases[ 0 ] ), " first failure: ", firstFail ); + CHECK( firstFail.empty() ); +} + +TEST_CASE( "strkern: F2 tokenizer == pre-change walker (spans + fused hashes) on the random corpus" ) +{ + const Sweep& s = sweep(); + INFO( "buffers: ", s.bufferCount, " first divergence: ", s.tokFail ); + CHECK( s.tokFail.empty() ); +} + +TEST_CASE( "strkern: G0 the real-text corpus is big enough for the arms below to mean anything" ) +{ + const RealText& r = realText(); + INFO( "files under ", repoRoot(), "/{src,docs}: ", r.fileCount ); + REQUIRE( r.fileCount >= 50 ); +} + +TEST_CASE( "strkern: G1 classMasks vs oracle over every byte of src/ + docs/" ) +{ + const RealText& r = realText(); + INFO( "files: ", r.fileCount, " bytes: ", r.totalBytes, " first divergence: ", r.classFail ); + CHECK( r.classFail.empty() ); +} + +TEST_CASE( "strkern: G2 lowerFoldAscii / lowerFoldedEquals over src/ + docs/" ) +{ + const RealText& r = realText(); + INFO( "files: ", r.fileCount, " first divergence: ", r.foldFail ); + CHECK( r.foldFail.empty() ); +} + +TEST_CASE( "strkern: G3 find3 / findByteset over src/ + docs/" ) +{ + const RealText& r = realText(); + INFO( "files: ", r.fileCount, " first divergence: ", r.findFail ); + CHECK( r.findFail.empty() ); +} + +TEST_CASE( "strkern: G4 tokenizer == pre-change walker over every byte of src/ + docs/" ) +{ + const RealText& r = realText(); + INFO( "files: ", r.fileCount, " bytes: ", r.totalBytes, " first divergence: ", r.tokFail ); + CHECK( r.tokFail.empty() ); +} + +TEST_CASE( "escape: escapeXml byte-identical to the frozen per-byte reference" ) +{ + const EscapeRun& e = escapeRun(); + INFO( "inputs: ", e.caseCount, " mismatches: ", e.xmlBad ); + CHECK( e.xmlBad == 0 ); +} + +TEST_CASE( "escape: appendCdataSafe byte-identical to the frozen per-byte reference" ) +{ + const EscapeRun& e = escapeRun(); + INFO( "inputs: ", e.caseCount, " mismatches: ", e.cdataBad ); + CHECK( e.cdataBad == 0 ); +} + +TEST_CASE( "escape: escapeInto byte-identical to the frozen per-byte reference (8 flag combos)" ) +{ + const EscapeRun& e = escapeRun(); + INFO( "inputs: ", e.caseCount, " mismatches: ", e.jsonBad ); + CHECK( e.jsonBad == 0 ); +} + +TEST_CASE( "escape: xmlScrubIsLossy(false) really means appendCdataSafe moved no byte" ) +{ + const EscapeRun& e = escapeRun(); + INFO( "inputs: ", e.caseCount, " disagreements: ", e.lossyBad ); + CHECK( e.lossyBad == 0 ); +} + +#if EMITESCAPE_MUTATE_BYTESET +TEST_CASE( "escape: MUT a byteset missing '<' DISAGREES with the reference (the gate can go red)" ) +{ + const EscapeRun& e = escapeRun(); + std::printf( " MUT: %zu of %zu inputs differ\n", e.mutDiff, e.caseCount ); + CHECK( e.mutDiff > 0 ); +} +#endif From 4d74e8116f10032f3c2821926b27eebfd6cc14c3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 20:37:28 -0400 Subject: [PATCH 40/73] feat(doc-drift,flags,flip,situ): three listing verbs learn to say what they dropped, and two learn to page it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 F-06/F-07/F-10 — the cap-correctness class PR #108 closed on `--edit-check`, found again on the three verbs whose row listings no flag could reach. The audit measured the silence; this measures it back: F-06 `--doc-drift` cut the per-doc listing at kMaxAnchorsShown=12 — 56 of 149 failed anchors on this tree — and disclosed it with nothing but a remainder. No output named the flag that lifts it. F-07 src/darkflags.h and src/flipimpact.h emitted ZERO shown=/total=/capped= tokens between them (grep, both files). `--flags` cut the read sites under a gate at 8 (3 gates cut here, 182 of 193 sites) and `--flip` cut six listings at 25, in silence, with no flag that could lift either. F-10 `--situ` — the mid-task verb CLAUDE.md's own protocol names — cut its blast radius to 8 of 11 files and its co-change partners to 8 of 28, said so in PROSE ONLY, and REFUSED --limit. WHICH ROWS ARE THE ANSWER, AND THEREFORE NEVER PAGE. Read out of each verb's own legend, asserted in the gate, and stated in band: --flags the rows ARE the answer ("what is BUILT but DARK here"); the read SITES under one gate are context. Sites page; gate rows never do. --flip the rows are the tests_to_run family — the rows you RUN. They were capped at 25 and are now served WHOLE on every page, exactly as --test-gate's listing always has been. The other six listings are context and page. --situ section [2], tests to run, is the answer: --test-gate exits 4 on those rows. Its 25-row cap is DELETED rather than raised (kSituTestRowsShown is gone) — a cap on an answer is the finding, not the number. Sections [1] and [3] are context and page. --doc-drift no answer LISTING; the answer is the per-doc verdict, computed over the full anchor set. THE AUDIT'S SUGGESTED FIX FOR F-06 WAS BUILT AND REJECTED, with the evidence coming from this repo's own contract gate. Routing kMaxAnchorsShown through effectiveRowCap makes --limit raise it — and the rows are a SECONDARY listing under the PRIMARY rows that --limit already windows, so one flag governing both makes the same doc print shown_failed="3" at --limit=3 and shown_failed="6" at --limit=6. page[0:3] + page[3:6] then stops equalling page[0:6] and pagingsweepcheck's --offset continuity arm goes red on doc-drift, correctly: a paged walk that is not equivalent to the whole is the §P8 bug that family exists to prevent. The tool had already settled this shape — pageview.h's kImportReachRowCap: a secondary listing "is NOT raisable by --limit … discloses through shown_importers=/importers_capped= and nothing else". So F-06 closes as a DISCLOSURE: the pair on the doc that was cut, plus next="--doc-drift --detail=1", the flag that has lifted this cap since the verb was written and that no output ever mentioned. listingpagingcheck (A) pins the non-raisability directly: the first element must be byte-identical at --limit=3 and --limit=6. WHAT LANDS src/pageview.h secondaryCutAttrs() — rule 6's pair, emitted ONLY on a cut (rule 3's --skill-scan shape), so 86 of 88 gates pay no bytes to say nothing was dropped. src/docdrift.h gains shown_failed=/failed_capped=/failed_total= on a cut (failed_total is drift= + dated=, spelled out rather than summed, and deliberately NOT anchors_total= — anchors= on the same element counts a different population, and reusing it would rebuild the dark=/dark_gates= collision §P8 renamed its way out of). gains shown_weak=/weak_capped= against its own n=. The root gains next=. A VERIFY asserts the per-doc verdict is taken from the full set. src/darkflags.h per-gate sites run through pageWindow (--limit=N raises the 8, --offset=M pages it, --detail still lifts it); gains shown_reads=/reads_capped= against reads=; the root gains next="--flags --limit=N", exact. src/flipimpact.h the six context listings run through pageWindow and disclose shown_=/ _capped= against their own n= (or lights' r=/b=); tests are uncapped; the near-miss "did you mean" list gains its TOTAL, so a suggestion list that dropped seven better names says so on the one output a lost caller reads; root next=. src/situ.h sections [1] and [3] run through pageWindow; the showing-note gains the machine triple shown=/total=/capped=1 and the exact `next: --situ=… --limit=N`, echoing the caller's own selector; section [2] loses its cap. src/cli.h --flags and --situ join honorsPaging (and leave kShapingVerbs — a verb cannot hold a row in both tables). --situ is the FIRST PROSE member: it has no XML root, so pagingsweepcheck (L) reads its sections as prose instead of parsing a root. MCP M13's rule, derived by mcpcontractcheck (G) from kPagingHonoringVerbs itself: the `flags` and `situational_awareness` twins declare limit/offset and honour them. The situational twin's default stays UNBOUNDED — that payload has always served every row, so limit there is relief for a caller who wants less, never a new cut. GATE FIRST: test/listingpagingcheck.sh, written before the code and red against 6afaa457 — 24 of 33 checks FAIL there, including every CROSSING half (crossing is proved from the run at --limit=1000000 / --detail, and the finding IS that the pre-change binary does not honour them). Six arms per verb: crossing, disclosure, RE-DERIVATION (the decisive one — the same binary twice, verdicts byte-identical with the bound removed), silence, answer-completeness, and a mutation control that rewrites a verdict to the window's row count on a SYNTHESIZED document (never a scratch build: a compile inside a wall-budgeted gate is a recorded failure mode). Registered in test/regression.sh; gate count 588 by docs/gatecount_build.py. NUMBERS doc-drift 56 rows bare -> 149 under --detail=1; base binary at --limit=1000000: still 56. flags 182 read rows bare -> 193 at --limit=1000000; 3 gates disclose a cut; 20,553 -> 21,497 B. situ [1] 8 of 11, [3] 8 of 28, both now with shown=/total=/capped=1 and a pasteable next:. LIMITS.md caps whose file discloses 100 -> 116; silent 106 -> 89. kSituPartnerRowsShown and kSituPartnerFileRowsShown classified OUTPUT; kSituTestRowsShown deleted. MCP tools/list 41,220 -> 41,830 B; ceiling re-anchored 41,300 -> 42,000 with the bytes attributed tool by tool in mcpmanifestcheck's header (flags +329 = 184 schema + 145 description; situational_awareness +281 = 184 + 97; nothing else moved), 170 B headroom. byte-identical 12/12: --top-k=100000, --for, --grep, --pack-task on ripwire's own tree, the go corpus and ctxpack, old binary vs new, cmp clean. VERIFIED listingpagingcheck, pagingsweepcheck, truncvocabcheck, capdisclosurecheck, limitstablecheck, nextverbcheck, morecontractcheck, collectioncapcheck, floormarkcheck, legendcoveragecheck, legendcostcheck, compactlegendcheck, docdriftcheck, flagscheck, flipcheck, situdiffcheck, testgatecheck, testgatepagecheck, shapingflagcheck, modifierguardcheck, argvdiffcheck, clicheck, helpbudgetcheck, flagtablecheck, flagsurfacecheck, docscommandscheck, manifestcheck, gatecountcheck, printffmtparitycheck (re-pinned: help_all only), xmlwellformed, plus every gate --test-gate named (mcpcontractcheck, mcpmanifestcheck, mcpverbscheck, mcpattrparitycheck, mcpstrictschemacheck, mcptranchecheck, mcpw2fixcheck, mcpclidiffcheck, mcpeditpresencecheck, mcpframehonestycheck, binoverridecheck, cacheidentitycheck, emittertruthcheck, emptyvaluerefusecheck, fixedbufsweep, grepbytescheck, hookcheck, htmlhostcheck, loopconservationcheck, nulbytecheck, panellegendcheck, precedencecheck, qualitypanelcheck, readmedriftcheck, rootrelcheck, showcasecapturecheck, sincecheck, sliceflowcheck, testrowruncheck). --quality-delta exits 0; determinism 2x on the map and on all four changed verbs; xmllint clean on the map and on doc-drift / flags / flip. test/morecontractcheck.sh's gate_reads_attr took the LAST reads="…" on the tag with a greedy .*, which the new shown_reads= made wrong; it is anchored on the leading space now. ripwirepubliccheck arm 3 fails identically on 6afaa457 (a §-coordinate in a src comment that lane H's widened LIMITS census now surfaces) — pre-existing, not this lane's. ONE COMMIT, NOT THREE, and the reason is the gate: the three fixes share secondaryCutAttrs, one honorsPaging edit, one paging-sweep table and ONE gate file for ONE class. Splitting them produces intermediate trees where listingpagingcheck is red, which this repo's own discipline forbids. Co-Authored-By: Claude Fable 5.1 --- .ripwire_quality_acks | 74 ++++- README.md | 4 +- docs/COMMANDS.md | 10 +- docs/EVALS.md | 6 +- docs/LIMITS.md | 71 +++-- docs/limits_classes.tsv | 2 + present/deck5_ripwire_build.js | 6 +- src/cli.h | 29 +- src/darkflags.h | 65 ++++- src/docdrift.h | 85 +++++- src/flipimpact.h | 149 +++++++--- src/mcp.h | 32 ++- src/mcprefusal.h | 4 +- src/mcpverbs.h | 39 ++- src/pageview.h | 37 +++ src/situ.h | 82 ++++-- src/verbs_change.h | 25 +- test/docdriftfix.golden.xml | 2 +- test/listingpagingcheck.sh | 502 +++++++++++++++++++++++++++++++++ test/mcpcontractcheck.sh | 6 + test/mcpmanifestcheck.sh | 20 +- test/morecontractcheck.sh | 5 +- test/pagingsweepcheck.sh | 25 ++ test/printf_parity.manifest | 2 +- test/regression.sh | 2 +- 25 files changed, 1116 insertions(+), 168 deletions(-) create mode 100755 test/listingpagingcheck.sh diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index b3b79a629..603ae20a3 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -8,6 +8,7 @@ ack api-surface 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 M12 (capture-audit L9): ack api-surface 105c48e20c80c896 3 cid=720fab31ea99ebde A2: unmeasuredHintNote gained the AbsHintFrame parameter one commit after this lane introduced it (4db6fb3). It is a header-inline helper in namespace mcpedit with exactly one caller, resolveOneForEdit, in the same file; no consumer outside this lane ever saw the 2-arg form. The widening is what makes the never-parsed disclosure and the symbol scan agree about which files a hint names -- two copies of that rule is the defect this replaces. ack api-surface 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept ack api-surface 10f47dd5a3f35d86 5 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history +ack api-surface 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 15754e3561a34f40 7 cid=e3721579f68947f6 deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack api-surface 163c0a0eb3219fa9 5 cid=9e7d5dab8c14a887 R2: prEmptyRootTail gains the truncated= parameter it needs to carry budget-floor-exceeded — deliberate, 1 caller, incompatible=0 (--edit-check contract-change); prEmptyRootPrice is the new file-scope helper that decides the label and re-prices, keeping writePrContext's own complexity and LOC unchanged | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack api-surface 1689c98fa4eac33e 4 cid=f5ec9b69e2526e08 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -34,6 +35,7 @@ ack api-surface 2e6026bd58111ad5 5 cid=3d6b010d178e2c8f by=src/* lane/n6-d, the ack api-surface 30dfe3580e3235a8 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 32e780668c108fa5 5 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack api-surface 33d55f3b93bc79ea 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack api-surface 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 3561d0281d324276 14 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 3703c22e2f2112bd 5 cid=e07b67ecf3068d1d by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack api-surface 3877dd1e9b4ae997 4 R-E (2026-08-17 harvest): narrowLegoToRenderedSigs needs an explicit rootPrefix param because packSignatures' sigsRendered rows are already root-relative while the function's own escaped path comparison was still absolute -- every comparison silently failed, narrowing legoScoped to nothing on every --for run whose rendered sigs hit this path. The +1 param (defaulted, so every other caller is unaffected) and the small cx/LOC bump on narrowLegoToRenderedSigs and its one caller runForLens are the minimal fix; caught by legobundlecheck.sh going red for the wrong reason. churn=self is this lane's own edit window. @@ -41,6 +43,7 @@ ack api-surface 39588f57bd7b46b5 6 cid=2116294439c0a50f M13 paging/budget parity ack api-surface 3a54ea98a485670c 2 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 api-surface 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack api-surface 3cb849a8c8a4aa2d 5 cid=fce85f6f2f55e1af lane V1 N2 (f5913f3): grepTierAttrs/grepTierKeys gain floorAlreadyEmitted, resolveCandidates gains capFired — one explicit parameter each, every caller updated in the same commit +ack api-surface 3d87404c1cdf50ec 3 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 3da8557837c77591 7 cid=d9d62f11f9760dc1 by=src/* §N6-C .gitignore-by-default: the crawl gains an ignore mode. The two api-surface/params rows are ONE deliberate contract change — ingest()/collectSources() take a trailing defaulted respectGitignore, the only way a CLI flag can reach the crawl without a global; the three short-horizon-churn rows are this lane's own edits to the flag ledger, the crawl and the --skipped verb, which is what adding a flag with a disclosure IS; collectSources +3 ccx / +11 LOC is what remains after the probe, the mode and the prune fan-out were extracted into probeIgnoreSet/recordDirPrune (it was +15/+43 inline). ack api-surface 3eaf4cfffc6ac8b9 3 cid=7e40913eaabff86f rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack api-surface 43e576a9c6592de5 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -49,6 +52,7 @@ ack api-surface 44b41056b999b501 6 R-R root-relative emission lane: threading th ack api-surface 44b4c7b78ee480d6 4 cid=6da24121cdbb52f4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface 44bac7b0ded56eb7 3 one grouped astQuery walk for --lint three built-in packs: cacheFriendliness takes its captures as a param (+1, deliberate contract) because --lint is its only caller and its own corpus-wide read+parse+compile pass was pure duplicate work; churn=self on astQuery/mergeAtomsPack/mergeCachePack/runLint is this one change own edit window. Output byte-identical on a frozen C++ corpus (1075 files) and a pure-Python corpus; 14 lint-family gates green; warm --lint 1.34s to 0.42s ack api-surface 451442e5d1031096 5 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack api-surface 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 49ea9954a4e77a06 6 cid=cf442ef6cda47ce0 P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). ack api-surface 4b788e6f0a75bc80 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 4b8467d6559779e4 3 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history @@ -64,9 +68,11 @@ ack api-surface 574641dcc1bdf0ec 13 WAVE-2 close (2026-08-19), finding 3 of 3: t ack api-surface 5774f0f445361430 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 59f050855874875f 6 cid=79ec141006215e25 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 5a07390012b46e06 9 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack api-surface 5c2c4a2b311b8dba 4 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 5e5cc30bcbc1fb63 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 5ec38fbd414fa4d4 7 cid=6f20993f1b475898 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 61e5df9e1e40ff70 5 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 api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 654551bf984cc299 5 cid=913136f787574436 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 6e58b0a307757079 24 cid=cb5c8aaa7451a632 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack api-surface 75720b711509b5b9 5 cid=a54cd0ffa32a9952 lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked @@ -86,10 +92,12 @@ ack api-surface 84b6bfc164c989e8 2 cid=87c39ba5f968fb34 M21(a) sa sym=/p=: stale ack api-surface 851e83b4505f10f6 12 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 85dd951a8f730eec 4 cid=4e0fbe2c7a019909 P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface 861092ef53c6c2dd 4 cid=48c86b50ca49ddb4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) +ack api-surface 86db2ff4e22cae54 6 cid=b9c9829a2aac834e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 8a92173ded649e17 14 cid=f76e97c84ac7c168 by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack api-surface 8d58de9bb922f582 4 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack api-surface 925094be92085dae 3 cid=714cea1e1b31a1ae A6: rollbackMessage gained a 'cause' parameter one commit after this lane introduced it (57fe5fc). It is a header-inline helper in namespace rw::editplan with two callers, both in the same function in the same file; no consumer outside this lane ever saw the 2-arg form. The parameter is what lets the concurrent-write abort reuse the rollback disposition wording instead of growing a second copy of it. ack api-surface 92ac9caf38b8aab0 4 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh +ack api-surface 95cc88ca4aab7039 4 cid=25c411d4871bda46 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 96fdcdff2f0ff0f7 4 R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). ack api-surface 983814f2b5912a90 3 cid=fe2cfa34166f503a M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 9ebbafddddd086b4 7 cid=50e8788010378ebe capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -98,6 +106,7 @@ ack api-surface a783c75feea9f253 3 cid=7345e4b8de2407c9 2026-09-06 stranger-audi ack api-surface a8879e4655677c7d 4 cid=cb0c898f2646cc43 P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface a8b774025a21bdc6 6 cid=8c396521251254f3 M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. ack api-surface a8cd12b856dd8307 2 cid=666f9172fa28bbb0 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 api-surface ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface ad50354348e0b35e 7 cid=1ca210dbe9314b90 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface aeed75863f7b617d 3 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface b496a273ae1564ef 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -106,6 +115,7 @@ ack api-surface b689422adfc04435 5 cid=cdb3a6ea16c83186 lift-disclosure round (2 ack api-surface bb2c0b847815a0ca 4 cid=9eb5f97927595a07 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1: the MCP edit_check verb mirrors the CLI pre-apply preview through the SAME editpreview::run, so the two surfaces cannot answer differently (gate arm N pins them document-for-document). new_body is optional and defaulted; every existing call site is untouched and the verb stays readOnlyHint true — passing it previews, it never writes. ack api-surface bcb3377087f2e034 7 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface be8176514288abc7 2 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack api-surface c1e1d72154d6784f 5 cid=c495b60a16e1ce45 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface c22ce1db6b79b087 4 cid=3b28778477fcc103 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface c30037c3e4f345a9 3 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack api-surface c4d50b7393d16274 6 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. @@ -116,10 +126,14 @@ ack api-surface cb0f0b806aa1e4e8 5 WAVE-2 close (2026-08-19), finding 3 of 3: th ack api-surface cb7342964b38db9c 3 lane/r10-cheap-buckets (r10 GitNexus fix round, LB-A + LB-G). SIX gating rows, ONE lane, read one by one before acking. THREE api-surface contract-changes, all deliberate parameter additions that ARE the feature: (a) mcpverbs usesText 2->3 params, taking McpPageArgs exactly as impactText already did, because the MCP uses verb gained the same default site cap as the CLI and an MCP-only agent that reads capped=1 needs a hatch it can reach (mcpclidiffcheck LENS 1 pins the two surfaces' root-attribute sets equal, so capping one and not the other is a divergence, not a saving); both dispatch sites AND kMcpVerbFields updated in the SAME commit, verified by mcpclidiffcheck/mcpverbscheck/usescheck green. (b)+(c) serialize packSignatures 17->18 and packSignaturesJson 11->12, both taking a trailing hasRelevanceFloor bool, default false so every non---for caller is byte-identical (verified: default map, pack-task, expand, exemplar, recall, hotspots, callers, grep, impact all unchanged). The flag cannot be replaced by passing a smaller topN, because those emitters read topN==0 as ALL, so a query nothing scores on would emit the whole corpus. THREE verbosity rows are the new code itself, already cut twice in this lane: duplicated rule bodies hoisted into relevanceFloorCut/pathTierIndexOver/compareTierThenPath, then the restated rationale moved to those helpers' headers - together taking gating from 13 to 6. What remains is runForLens +10, runCallHierarchy +11 and usesText +14 lines of genuinely new behaviour (the floor cut and its note plumbing; the tier index, the page window and the conditional legend clause). Splitting runForLens is a real refactor of its own - it was 658 lines before this lane touched it - and does not belong in an output-composition fix round. ack api-surface d1e50dc6d815cf27 6 cid=574545e642c44ece lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. ack api-surface d3b5cec59d7a7684 5 cid=5b456bf45dd0d9c9 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) +ack api-surface d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack api-surface 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 api-surface db9af56c7a3f5bee 4 cid=bec957a0aa99147d P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface dd2f935e1818171a 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack api-surface ddc3e2a475f782a1 4 cid=4333f578e348e95f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack api-surface e57abf94a3b1f3a4 5 cid=788aa47a3ec3d3f2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface e6bdc5187566f3d7 8 cid=615b35e39fe420e4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface ed05ee0357d016f1 4 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. @@ -170,6 +184,7 @@ ack api-surface:new-symbol 222f47325b7191d9 0 cid=883aa0365c34d51e timsort vendo ack api-surface:new-symbol 246cc6ad10ff06bf 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack api-surface:new-symbol 25c9f0504fd376a4 0 cid=61a6b84838b35798 R1 lane V2, remainder: four api-surface new-symbol rows are the deliberate extraction this fix chose over inline growth — rw::kOverCeilingLegend (ONE wording for three surfaces, hoisted out of a function-local constant the MCP twin could not reach), rw::priceForTaskRoot (the MCP for root's price-and-label step, lifted whole out of a 337-line body), forLensJsonBudgetStanza and forLensJsonOverCeiling (the forLensNotesStanza/forLensJsonTailStanza precedent in the same file). None widens a shipped CLI or MCP contract: no new flag, no new verb, no changed signature. verbosity runForLens 990 to 993 is three comment lines recording where the over_ceiling legend wording now lives. ack api-surface:new-symbol 26aa7c55d54c4341 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean +ack api-surface:new-symbol 26e3f9a6bff9ea57 0 cid=942faaac6ddd5389 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 27e7bddec77b7abb 0 cid=52e76f37716b687a lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack api-surface:new-symbol 2978aaf0cb86e41e 0 cid=54ff85631f901cea or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack api-surface:new-symbol 2af9a541302cfd51 0 cid=a8ea687c59e866e3 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. @@ -220,6 +235,7 @@ ack api-surface:new-symbol 62fc004629681a50 0 cid=bb30f060c75deab1 M13 paging/bu ack api-surface:new-symbol 6360fe1b523a5602 0 module-constant round (2026-08-12, test/moduleconstcheck.sh): the four short-horizon-churn rows are the documented extraction-bump protocol itself — kParserVer and its quality.h mirror MUST move in the same diff (qextractionkeycheck), dropConstantCapture is the policy function this round exists to change, and cudaMemorySpaceQualifierOf's edit is the dedup the quality gate itself demanded (169-token clone dissolved into childTokenAmong). The 24-token ncBoolTypeName|cudaMemorySpaceQualifierOf pair is a cross-domain wrapper-shape coincidence (naming-lens vocab membership vs tree-sitter child scan over disjoint token sets in different files); merging them would be the wrong abstraction the delta header warns against. ack api-surface:new-symbol 63e57d342c401094 0 cid=f31eb03cde31c7a4 E1 (terminality round A, lane E): the seam rules' disclosure record (trailingNewlineFolded, separatorPadded) ack api-surface:new-symbol 64a91ab838dd6c45 0 cid=c371f3609107eee5 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions +ack api-surface:new-symbol 659af2613fd4ba75 0 cid=a69e85d30342478d C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 6659666bc5b97fd1 0 cid=b90dac55f3b271a0 main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical ack api-surface:new-symbol 68442823ab40d356 0 cid=86fc9a0af44d3bf2 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface:new-symbol 69a0fdb34357274c 0 cid=2b6f842c43e01e62 cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. @@ -231,12 +247,15 @@ ack api-surface:new-symbol 6cd1fd58a259ee2a 0 cid=ccf5a904f4638db2 M1: a 36-toke ack api-surface:new-symbol 6f27216622124253 0 V1 harvest 2026-08-15: withFileContext is an opt-in trailing param (default false) for the sibs=/inc= feature -- additive, backward compatible, every existing caller unaffected ack api-surface:new-symbol 6f6bb3063be1e4c6 0 cid=77b0be7127b0858f E1 (terminality round A, lane E): the terminator a seam spells (CRLF vs LF), so padding matches the file ack api-surface:new-symbol 7244cb7d8ff2b89f 0 cid=54a576dd82024bc7 wave-3 close, H7 hosts (substrfiltercheck --plan/--abi arms): printStrayFilterNoMatch is the ONE sentence for a --stray-content filter that selects no ref, spoken by the bare verb and its two hosts; it shares refuseFlagValue's fprintf shape (33 tokens) but not its contract — arm C pins the 'not a measurement' clause the value-domain sentence lacks +ack api-surface:new-symbol 73690330b833c9fa 0 cid=e5b6815c0a360d29 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7477bca827a815da 0 cid=56abaf5da4a1419d 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 api-surface:new-symbol 74bb913f25167779 0 cid=b28f2989408cb8a5 V1/R2+N4: the five helpers this fix names at file scope (prLegendText, prRootOpenText, prAnchorNoteText, prPriceDocument + PrPriceCtx, kPrCloseTag) are the emitter's own bytes made measurable — every one returns what used to be fprintf'd so est_tokens can price it; writePrContext 371->376 LOC is the net of that extraction (it lost the 21-line legend and gained the price context). ack api-surface:new-symbol 7595f3b659793aee 0 cid=ff0af690126b5e0e 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 api-surface:new-symbol 76cea06ffde88328 0 cid=ceae430130676ebb idiom-class clone: a NAMED one-line std::find predicate. What matches is the std::find idiom itself — 36 normalized tokens, zero shared domain identifiers with ncAnyOf/namesNode, different value types, different subsystems. Inlining it at its two call sites to dodge the row would be metric-gaming; the name is the documentation. +ack api-surface:new-symbol 7723ed789a1c9c09 0 cid=7886e44cd18fba0b C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7857b2ae91f6f363 0 cid=87b7efc7bdc2ece7 F4 (lane F): three new symbols in src/prcontext.h — prEstTokens (ONE estimator for every pr-context root, replacing the ladder's inline formula so the empty root cannot price differently), kPrEmptyDiffBody and prEmptyRootTail. Extracted deliberately: composing the empty root's budget tail inline grew writePrContext (already ccx 154 / 371 LOC) by 2 complexity and 12 LOC; as helpers it grows by neither. No new CLI or MCP surface. ack api-surface:new-symbol 79625906f9f71ad0 0 cid=eee9afc7f54a3a7f E1 (terminality round A, lane E): the ONE spelling of the redaction marker the write surfaces refuse +ack api-surface:new-symbol 79cf899b082e5c8d 0 cid=99549639c3506907 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7a04eee0ff6ec2d7 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack api-surface:new-symbol 7abc8c5ec9dd63a0 0 cid=f43fc221f7b8e8f6 E2 (terminality round A, lane E): SHA-1 modular add in uint64_t and masked: -fsanitize=integer flags an unsigned 32-bit wrap (found by the asan tree) ack api-surface:new-symbol 7bbb17371a4af833 0 cid=d834ff81483d34ad 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. @@ -251,6 +270,7 @@ ack api-surface:new-symbol 82dae23ff5754ef2 0 cid=d72d8662acd735e0 M1: three nam ack api-surface:new-symbol 839f231a03346895 0 cid=ccd63911f3efb68f M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface:new-symbol 8483c65facce2008 0 cid=b6d7989b6d74800b R1: four new file-scope helpers in the mcpedit namespace (kRedactionEllipsis, countRedactionMarkers, redactionMarkerRefusal, redactionMarkerRefusalFor) replace the two constants the substring scan used; they are the shared predicate the three write surfaces call, not a widened public contract ack api-surface:new-symbol 8565479e9b57fb76 0 cid=06bbaf2bb0c34f1a by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. +ack api-surface:new-symbol 882ef9b9b64fd2c1 0 cid=62a45d4a835024bc C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 8832b6f56a77fdcc 0 cid=27c019875ae26b94 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 api-surface:new-symbol 8ecb190954dce18a 0 cid=ba2fa2ff81d48d5b 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 api-surface:new-symbol 8f70082cef086db9 0 cid=1e87f5e854727c5e 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 @@ -265,6 +285,7 @@ ack api-surface:new-symbol 99ae345ce0cf2182 0 cid=9afdf4c38e8ded3d lane/tc-slice ack api-surface:new-symbol 99e507e0c233d21e 0 cid=9948b608a4486528 E2 (terminality round A, lane E): SHA-1 primitive (git identity, not security) — zero dependencies ack api-surface:new-symbol 9cd214c2e0f6166d 0 cid=027e9c437962f1e2 E2 (terminality round A, lane E): the region budget (2048 B) — over it head/tail/elided_lines, capped:true ack api-surface:new-symbol a04db81aba61fefa 0 cid=e425b3c91591ce8c or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS +ack api-surface:new-symbol a10ea8d3bdca1dfb 0 cid=a64af151dfacd435 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol a32e1579d490db64 0 cid=d11b5bd0646c341f E3 (terminality round A, lane E): the overwrite child's budget (4096 B) — over it head/shown/capped=1/elided_lines ack api-surface:new-symbol a45fd373062fc100 0 cid=d5b6e91fa1f1eb6d by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack api-surface:new-symbol a4a8c133e40fe0ac 0 cid=0dfc15f8d6edef3d M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -283,6 +304,8 @@ ack api-surface:new-symbol b7ef640cb4f56805 0 cid=37998fcb7c29fd47 M13 paging/bu ack api-surface:new-symbol b996fd75c7b88176 0 cid=f1d29a397ab28c86 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 api-surface:new-symbol c02fa9205afe3baf 0 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 api-surface:new-symbol c0e0ed1f0514d6fe 0 cid=90c371f88c914eac P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. +ack api-surface:new-symbol c26a94415f28768f 0 cid=a0746b8e350afbd2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack api-surface:new-symbol c378ae8278c2ec12 0 cid=d22db6e5cdfe9bfe C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol c4398806f7cd6b15 0 cid=8d80db4bdc937a7f by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. ack api-surface:new-symbol c705c286654dc569 0 cid=4323d40ad174ff5a cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. ack api-surface:new-symbol c7719202f0554d70 0 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. @@ -422,12 +445,13 @@ ack complexity cb7342964b38db9c 33 cid=3af54969638510bc by=src/* member-variable ack complexity d03215f48bec3886 17 cid=d622ffa6d33014f4 E1 (terminality round A, lane E): +1 branch: the redaction-marker refusal beside the NUL one (one ladder, one vocabulary) ack complexity d295ac7080c14dd5 18 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity d42c85b67bd0956f 52 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement -ack complexity d63db6944aa504a7 511 cid=38668b9f7642e38d F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. | prior: capture-audit 2026-09-04 wave-2 merge, lanes L6 + L8 grew the MCP dispatcher past each other's acked magnitude (each lane's own e3b52d3..lane delta was acked to gating=0): L6 M13 (limit/offset/budget_tokens on the seven paging twins, callhierarchy.h shared with the CLI), H14 (lens= declarations, filter= echo), M5 (the verb:arg string grammar read behind isBatchCliSpec) and P11/§5a-3 (legend:compact on slice); L8 P17 (slice/edit_check batch arms) and P9 (post_check read on the three edit verbs). A dispatch chain is its arms; each arm is gated by its own lane's gate (mcpcontractcheck G, mcpattrparitycheck, batchcheck g/h, receiptpostcheck) +ack complexity d63db6944aa504a7 527 cid=1943300f4961480a 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: F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. ack complexity dbe6ed5269d328a4 130 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 complexity dd02b378ae6b5b75 30 cid=5a0958e5bca4aaf3 L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack complexity dd627540f10bba76 166 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity dda0db55532bd5e1 23 cid=848b85e47e6bd67c R1: +2 ccx on editplan::prepare and +1 on editpreview::run — ONE guarded early-return each (the redaction-marker gate moved to where the replaced bytes are known); both were already over the bar before this change and neither gained a nested branch | prior: E3 (terminality round A, lane E): +1 branch/+8 lines: the preview appends its overwrite child before ack complexity dda343d7d36edaba 77 cid=e8803c763bec269d L10: printLintRuleTallyRow's compiled= param and runLintRules' uncompiled-query mapping loop disambiguate a lint-rules query that failed to compile from one that legitimately found zero matches (see docs/PLAN lane L10, finding 3) — deliberate, backward-compatible (defaulted param, incompatible=0) +ack complexity ddc3e2a475f782a1 18 cid=4333f578e348e95f 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 complexity e14feca13c7ae680 57 cid=29dde7810cd9e120 M4 (lane ca-L2): writeHandoffPacket +24 LOC for three emitted facts (run= on , detached=1, candidates=/capped=) and the comments recording why the note match changed; complexity held at 56->57 minor by extracting verifiedNoteTargets() and kHandoffLegend | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity e1b4964cea59171b 18 cid=8ba981522099e145 H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. ack complexity e7f50422948f2c09 17 cid=6c7e771d42d81c16 round ec5e3c3..HEAD, lane L7 P4 (defaultceilingcheck, prbudgetcheck, treecheck, usescheck 5b) + close H7 hosts (substrfiltercheck): default ceilings — pr-context budgeted by default with a windowed file page, --around depth 1, --zoom levels_shown, --external-surface 100 rows + builtins_excluded=; runChangeViews also hosts the pr-context paging and the --plan/--abi no-match refusal branch @@ -554,6 +578,7 @@ ack duplication e75742478839369d 31 timsort vendoring: every row is the vendored ack duplication e91c5e004c251a91 24 module-constant round (2026-08-12, test/moduleconstcheck.sh): the four short-horizon-churn rows are the documented extraction-bump protocol itself — kParserVer and its quality.h mirror MUST move in the same diff (qextractionkeycheck), dropConstantCapture is the policy function this round exists to change, and cudaMemorySpaceQualifierOf's edit is the dedup the quality gate itself demanded (169-token clone dissolved into childTokenAmong). The 24-token ncBoolTypeName|cudaMemorySpaceQualifierOf pair is a cross-domain wrapper-shape coincidence (naming-lens vocab membership vs tree-sitter child scan over disjoint token sets in different files); merging them would be the wrong abstraction the delta header warns against. ack duplication e94fce0d5caa811d 66 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 ea4ae03ca699bce4 35 lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). +ack duplication ec3f7b5523723637 110 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack duplication ef3d5272b705b505 107 ingest.cpp split 2026-08-29: pre-split clone pairs (buildNewlineOffsets vs the bench_newline_ab arms, acked lane-B3 at keys 98099b517e9d2fbb/a6f7de52f80c9f48) whose clone-group keys changed because buildNewlineOffsets moved VERBATIM into ingest_astquery.h — the disclosed clone-ack rename floor, same artifact as the main.cpp split's moved-clone row; argvdiffcheck vs c267a4b proves no body changed ack duplication f14cfce02be351ad 24 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication f3518ef93569f6ae 25 idiom-class clone false positive: a three-way ternary over string literals, 25 normalized tokens, sharing no domain identifier with macroRoleAttr and in an unrelated subsystem. Reading both confirms it. @@ -609,11 +634,13 @@ ack params 08d796d1b006560c 6 E1 answer grader + questions task source + claude- ack params 0a3d16d6f3139408 10 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack params 0d7b8741392284d1 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack params 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 19d43d944ddcd186 6 cid=533f2648b2fac227 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 19e15f944de795a8 6 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 3561d0281d324276 13 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. +ack params 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 4b788e6f0a75bc80 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack params 4d421276056367a9 6 cid=5a1a2f3caf33a19e timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 5361e2bced6f1988 8 cid=9cf2c52c10b9d878 M12 follow-up (capture-audit L9): --ensemble gained root=/root-relative p= — writeEnsembleReport's 3 new default-valued params (singleRoot/rootPrefix/rootAttr, back-compat) thread the caller's already-computed single-root spelling through; short-horizon-churn on the touched dispatcher. @@ -622,20 +649,24 @@ ack params 5391ffd9aa5765bf 6 cid=8a2aed10e80ed270 P2.2 register-macro dead-code ack params 5710beada2095a34 9 cid=3371683faa811117 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack params 59f050855874875f 6 cid=79ec141006215e25 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack params 5ec38fbd414fa4d4 6 cid=35883c203ba33e28 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 params 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 6e58b0a307757079 23 cid=912117812c5cc12d by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack params 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate ack params 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack params 851e83b4505f10f6 12 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. +ack params 86db2ff4e22cae54 6 cid=b9c9829a2aac834e 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 params 8a92173ded649e17 13 cid=a2c4a7904b16ec5b by=src/* A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack params 901cccac113c5416 8 cid=668f2c9bee810d42 main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical ack params 99ae345ce0cf2182 6 cid=9afdf4c38e8ded3d lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack params 9ebbafddddd086b4 7 cid=50e8788010378ebe capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack params a6718fdf5bbb9f01 6 cid=0a16bab15ec4d994 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params a8b774025a21bdc6 6 cid=8c396521251254f3 M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. +ack params ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params c02fa9205afe3baf 7 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 params ca97a4b6bf07887b 25 cid=72922b04b834af89 by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params cc1e357ae3abf4b0 6 cid=27af8baf185c5ada main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical +ack params d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params d9c7b2164cb0b8c5 6 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack params e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept @@ -658,6 +689,7 @@ ack short-horizon-churn 060a064b6ffa7775 44 W1-S2 churn-keying fix (pathQualifie ack short-horizon-churn 0749c4e602daa603 9 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 0777f290bdc69b11 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 07cf5773893d89b0 4 cid=d4a11b38ea395398 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. +ack short-horizon-churn 083de06af84b3a87 30 cid=82bfd94f1c162607 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 0a3d16d6f3139408 13 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack short-horizon-churn 0ab42b0dea75e0c1 4 cid=75fd6d9c66adca24 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) 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 @@ -676,9 +708,10 @@ ack short-horizon-churn 1273fad87a99a9f2 6 cid=e6e7ad831a784a6c M10 (capture-aud ack short-horizon-churn 129d3c8d5a763870 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack short-horizon-churn 131068a6cedf0864 10 cid=bef0a5079a2bf8e4 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit ack short-horizon-churn 15061a69cb5b451f 4 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack short-horizon-churn 1520fa02411735c3 4 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 15754e3561a34f40 39 cid=93c7392c7b677557 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack short-horizon-churn 1610c5acaa7d4806 6 cid=545658032a875c30 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn 1624b02e9104560e 151 cid=de44395b061b6b45 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn 1624b02e9104560e 154 cid=85e3075128763497 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 163c0a0eb3219fa9 10 cid=9e7d5dab8c14a887 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack short-horizon-churn 16e4fa1d32860233 9 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn 1930a35978b9543a 6 cid=b213f6ac9734c995 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. @@ -740,11 +773,13 @@ ack short-horizon-churn 3298be65bf6058ef 100 cid=0d8a43a6b93b2f31 M1: self-churn ack short-horizon-churn 32d067796a0393b8 32 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn 32e780668c108fa5 33 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 3385856c74077f1a 22 cid=ef5ec129660c61f5 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. +ack short-horizon-churn 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3561d0281d324276 29 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. ack short-horizon-churn 357ab167dccb9a4d 2 cid=ebe308d04e543d16 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn 3703c22e2f2112bd 7 cid=e07b67ecf3068d1d by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn 3797b511eae7d123 46 cid=52d7844487406324 fix-round follow-on: the recall-ceiling MCP branch (+3 ccx on the dispatcher), the apostrophe word-boundary guards in firstQuotedLiteral (the refusal logic IS the fix), and re-touch churn on the flag table / RecallShape comment / installer timeout re-pin ack short-horizon-churn 37f4883f917f57a8 4 cid=66b110baf5a52e2b wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file +ack short-horizon-churn 380b7de5df1cfd73 6 cid=5c6aaf66598466c8 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 38a5abd610359cfc 20 cid=8cef749bc3ef723e churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. ack short-horizon-churn 38c814a492cc9c8a 4 cid=b804c98a2da4bdec by=src/* A2 (dropped_positive, 2026-09-03): emitForLensJson gained the droppedPositiveStanza, mirroring the existing overCeiling/notesStanza envelope-key shape; self-churn is this round's own fresh edit. ack short-horizon-churn 39680772720129ac 3 cid=7419620b90eb7a8e by=src/* round-4 F-01 (--edit-check false contract-change across files): the ONE gating row is short-horizon-churn churn=self on editCheckContractVsHead, i.e. the footprint of having edited a function this round-3 window already touched — not new debt. The fix itself is a two-line predicate swap: nowDefs is now counted under computeSnapshot own qualityKey behind the same has-a-canonical-id presence gate, so defs_was and defs_now bucket identically and the documented invariant defs_was == defs_now on a clean tree is true rather than merely asserted. Verbosity was NOT acked: the bug explanation moved onto the function doc comment instead, which put editCheckContractVsHead back at its baseline LOC. Gate: test/editcheckcheck.sh arm (j), red-first on all four assertions, Python and C++ two-file fixtures, clean tree AND a real edit in one of the pair. @@ -756,6 +791,7 @@ ack short-horizon-churn 3be1c13661e5a63c 5 pack-task budget round (verifier K1+K ack short-horizon-churn 3c06a3d024349e5f 5 cid=f88a6b24c26cb6b9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3c07d993bfdbce53 8 cid=3718024f74a80d86 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked | prior: lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack short-horizon-churn 3c7a8e2ee4351734 11 fix-grep lane 2026-08-15: this symbol's ONLY change is the boolean line-scope branch swapping the 512-byte-capped DISPLAY helper grepMatchedLine for the new uncapped grepWholeLine. That cap was the bug in both directions (a required --and term past byte 512 dropped a real hit; a forbidden --not term past byte 512 failed to exclude its row), so churn=self here IS the fix and nothing else. Proven by an independent oracle: test/grepandcheck.sh (3a)/(3b)/(3c) now derive truth from /usr/bin/grep over a fixture whose second term sits at ~col 640, and all three arms are RED against the pre-fix binary and green after. Identity restored on this repo: --grep=stale --and=stale 289 -> 292 = plain --grep=stale; --grep=symbol --and=symbol 3028 -> 3030; the five mcp.h sites at cols 513-790 the verifier named are back +ack short-horizon-churn 3d87404c1cdf50ec 88 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3e0a074094789d60 15 cid=1556a2e06eef44c3 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 3e7c1221865b5b52 13 cid=f636d73c8428270e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3e91cee9f02f22a2 12 cid=76f4b09ef77556bd R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. @@ -777,6 +813,7 @@ ack short-horizon-churn 472cc93317130a8b 6 cid=dcdfa60eb93788ed OPTREMARKS F3 (d ack short-horizon-churn 476ab6f670e5d871 13 cid=bb27e8c78edbdb2f OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 48c3932de16d9cdd 9 cid=51e0c2d620ad5d17 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack short-horizon-churn 4975dcbd128411d7 5 cid=9b40c150a9616ae3 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 49e172c2aa455e68 4 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 49e838ca0844258b 5 cid=9d23ed0d32fea19d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 49ea9954a4e77a06 2 cid=ad0e2ee1266299ec selector-parity + degraded-routing round (2026-08-30): churn=self on emitGrepReport and classifySkipHealth is this one edit window — each was edited twice within the round because the round's own quality-delta demanded the second pass (the parse_degraded= inline predicate and classifySkipHealth's errNodes test both re-routed through the ONE fileParseDegraded predicate hoisted to model.h, so the three degraded surfaces cannot drift). Final state carries no complexity/verbosity/duplication finding; selectorscopecheck 8 arms + degradedhintcheck 8 arms green. ack short-horizon-churn 4a7106e488a2aa80 7 cid=a74981bb688153d7 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 @@ -811,6 +848,7 @@ ack short-horizon-churn 5774f0f445361430 8 graphrag-recon idea #1 corroboration- ack short-horizon-churn 578fa051307418ee 5 cid=307a821a72c35f00 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 59f050855874875f 14 cid=dbe0701cc173e389 by=src/* R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. ack short-horizon-churn 5b224c7fe142bd56 37 cid=3352105024f32829 by=src/* lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: issue #61 disclosure lane: the only gating row is short-horizon-churn on runForLens (churn=self) — the --for XML emitter is where the over_ceiling verdict has to be computed, so editing it again this week is the fix, not thrash. The predicate itself was extracted OUT of that body into the file-scope forLensOverCeiling beside its JSON twin, which is why the verbosity row dropped to sev=minor (+6 LOC, all of it the call and its pointer comment) instead of carrying the whole METHODOLOGY §9 argument inline. Gate: test/formaxtokenscheck.sh, written red-first. +ack short-horizon-churn 5c2c4a2b311b8dba 5 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 5d8f5288f0df10f7 95 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact ack short-horizon-churn 5e16b9596d9bd135 45 cid=743b6f0afb91f3cc by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack short-horizon-churn 5e5cc30bcbc1fb63 5 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. @@ -823,7 +861,8 @@ ack short-horizon-churn 61d6cde8defa73ad 7 cid=5d0a21a4321d8300 OPTREMARKS F3 (d ack short-horizon-churn 61e5df9e1e40ff70 13 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack short-horizon-churn 623e9c51c095e307 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 624a465290b8a040 3 cid=7b2469337b9ad141 lane E close (terminality round A): run_editsuite.py: the ripwire half split into classify_ripwire_call; remaining rows are churn on the change's home -ack short-horizon-churn 639de1c3670999f9 23 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) +ack short-horizon-churn 6302e2e27e23bcde 4 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack short-horizon-churn 639de1c3670999f9 30 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack short-horizon-churn 649ed79cefcd6fc6 6 cid=a3636823a3a67d9f by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn 6652d5114718eb63 7 cid=497a477af2561c0a OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. 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 @@ -856,6 +895,7 @@ ack short-horizon-churn 7c0cabd952bf3bd0 5 R-J: genuine feature growth in emitGr ack short-horizon-churn 7c0e356e60b323ba 41 cid=02317a43a1806043 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 7cc5608fd1dba918 6 cid=d15d69ecd7b55322 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 7d68e725e85e246c 5 cid=a38b17af7ad94ae5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 7d760d428aab46d1 6 cid=bb2c5ceed4e8efd8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 7e26e5533d486c7a 13 cid=aa596d9bb05d621d cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. | prior: P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack short-horizon-churn 7ed8ad2c213537a4 24 cid=959b21f28b01efe9 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 7efd6731993172f3 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. @@ -868,7 +908,7 @@ ack short-horizon-churn 815fbf65eea5df47 5 cid=98131c6d45bba16a OPTREMARKS F3 (d ack short-horizon-churn 8192a44ad5eb2510 3 cid=ff366a47a1cdb76d by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 81efad81c80fc1cd 10 cid=99f4094a45b32fa2 F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack short-horizon-churn 824e30c136361009 4 cid=11f0e44f31665844 wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file -ack short-horizon-churn 82b1e3c6a4919914 15 cid=9a163fb0cde77116 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: 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 82b1e3c6a4919914 16 cid=d0582bc76ff56c3e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 82bf48718457b826 2 cid=36b75364752453aa L10: --doctor legend + blobs_floor= disambiguation (finding 6) — DoctorCacheStats gains capHit, doctorCacheStats sets it, runDoctor emits blobs_floor= and the new legend comment; short-horizon-churn and the verbosity bump are the direct, deliberate cost of that ack short-horizon-churn 83f27ab44a2fb8a7 2 cid=7b1641f001fe32cd P7 (terminality round A, lane R): droppedpositivecheck's verify_exact re-pinned to the FLAT --for --json sigs array (one row object per ranked symbol, no {p,symbols} wrapper) — this lane's own gate edit, not drift ack short-horizon-churn 8430c0a1b20d242e 13 cid=3edc3a9877090050 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. @@ -934,9 +974,10 @@ ack short-horizon-churn a7ec845d7950d0b5 6 cid=383e339bb184adff OPTREMARKS F3 (d ack short-horizon-churn a8b774025a21bdc6 83 cid=fa890c64b85771c2 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. ack short-horizon-churn a9f76a08efdb3d50 24 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn aa71fcdf69942431 66 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. +ack short-horizon-churn ab7737f3582352d5 4 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ab9b3f1db516af63 21 cid=0b950315073c2901 by=src/* M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. | prior: A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn ac88b70b8c51cc70 14 L2 stale-ack disclosure: unavoidable growth/self-churn on runQualityDelta and qualityDeltaJson, the two pre-existing quality-delta dispatch hubs every new axis has to touch; logic already extracted to quality.h (staleForXxx/staleAcksXml/staleAcksJsonArray) to minimize the added footprint -ack short-horizon-churn ac9a2be19aa5cd79 151 cid=a601a09adfcfe345 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn ac9a2be19aa5cd79 154 cid=842f93fca4a9a61f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn adc4f10887c0a91d 20 cid=552e72bf148f6ab9 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack short-horizon-churn ae1f15d03c3d4faa 9 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 b0faf2a94fa4cc2d 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. @@ -950,7 +991,7 @@ ack short-horizon-churn b4e161be7a843fd6 5 cid=2871d9a64ac68a01 OPTREMARKS F3 (d 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 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 151 cid=9a5d0ccafb5fee94 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn 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. ack short-horizon-churn b7da91c05624ab1b 6 cid=12ba71ea2a2cbf3d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn b7e9e25ba704623a 2 cid=7a87da721dc19be4 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. ack short-horizon-churn b8c5550b5e3dc150 6 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) @@ -970,6 +1011,7 @@ ack short-horizon-churn c0d15239c2c4709d 24 cid=acf0020a7976fbe1 2026-09-06 stra 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 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 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). @@ -989,15 +1031,16 @@ ack short-horizon-churn ce968bdf8848c214 13 cid=3c5d6c07cbc4425e OPTREMARKS F3 ( ack short-horizon-churn d03215f48bec3886 3 cid=c7f3e74cf288dfc2 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper ack short-horizon-churn d05476877df0016e 6 cid=94feac15a58025e5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d3afca728392d688 6 cid=5765fc3aca7af273 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn d42c85b67bd0956f 15 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) +ack short-horizon-churn d42c85b67bd0956f 28 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) ack short-horizon-churn d44595768a7cf3af 2 cid=21539523996cfb13 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn d557a0077677ebd4 39 cid=7e6e8b041d54e368 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical -ack short-horizon-churn d63db6944aa504a7 34 cid=5d8c919443f11e2b answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: 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 d63db6944aa504a7 35 cid=1943300f4961480a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn d6e55b78e3a5fdda 7 cid=31161fe2ed1af5a5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d8aaf90801200a68 13 cid=d059a10443da1f80 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d9990dc7492a8bb2 20 cid=4646606f648e387e by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn d9e6c64c181ffc32 8 cid=5fb1e641981216da by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). 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 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. @@ -1023,6 +1066,7 @@ ack short-horizon-churn e750b88b46822476 2 cid=50e93e1770abeaaf by=src/* Phase 5 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 ed0cbdfe7fa72e8c 154 cid=f42bf8683228ffff C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ed10de1685178c0e 12 cid=b144cdefb89bd164 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. ack short-horizon-churn ed4b3f43f7f19909 12 T3 disclosure-gap fix 2026-08-22: verbosity/churn on the two emitters + harness trace persistence are the registered disclosure's own bytes and comments; the gate-helper clone follows the self-contained-MCP-gate convention (every mcp gate carries its own mcp_call) ack short-horizon-churn eea83c3db0f03d69 39 cid=95634463181e8692 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. @@ -1048,6 +1092,7 @@ ack short-horizon-churn fbdb0484f0707ba5 21 cid=ec37ab1a24846677 P1.2 is the sec ack short-horizon-churn fce48dedc40bc8eb 18 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 fd4cc5c6c0ad17d9 8 cid=9f660d42ad009523 C4 recall budget spreading: recall.h's budget loop was rewritten load-allocate-emit, which necessarily edits committed lines the 2026-09-04 capture-audit round last touched. The churn is real and correctly measured; the edit IS the fix for the defect those lines carry (recallbudgetcheck 8.1/8.2/8.9). Nothing to refactor away. | prior: markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack short-horizon-churn fd9dd2a5976fbf21 4 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 ff2ba74637bbbebf 6 cid=16a96df9744925a7 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 verbosity 05b5f1acf3a7e880 593 cid=10f4dfeb9f1d70cd P1.2 is the second half of one plan item whose first half landed in the immediately preceding commit of this same lane. scopeDisclosure gains a fifth parameter (the diff-expansion count) with one call site, both in this change; the churn=self rows are these symbols being finished, not thrashed. | prior: the scope partition is a feature added to a documented 600-line sequential pipeline. Every separable part is already its own named symbol: refuseUnusableScope, partitionByScope, refuseForeignAckSelection, quality::scopeDisclosure and kScopeLegend. What is left is the pipeline's own sequence. | prior: scopeless-fold lane: runQualityDelta verbosity 462 -> 474 is the MANDATED DISCLOSURE, not drift. test/legendcoveragecheck.sh refuses any first-screen attribute that its verb's own legend does not define, so the two new identity attributes (acks_rekeyed_by_scheme=, scheme_ambiguous=) had to be defined there or not emitted at all. The +12 lines are that definition, and they are prose inside a string literal in a function that was already 462 lines of overwhelmingly literal legend text. Extracting these legends into named constants would genuinely shrink the function, but every legend in main.cpp is written inline; hoisting one of them alone trades a size number for an inconsistency the next reader pays for, and several gates grep the legend text in place. Recorded as the cost of the disclosure, with the extraction left as its own refactor across ALL the legends rather than a drive-by on this one. ack verbosity 060a064b6ffa7775 621 cid=605cbb1f768828e0 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack verbosity 070de5367d6a5be0 87 wave merge 2026-08-20 (harvestexec): run_swebench_harness +11 LOC, from lane/outcome-harness-fixes. Every added line is DOCSTRING, and the trade it makes is the one this project wants: a 15-line 'TODO-verify' block listing what nobody had checked was replaced by a shorter VERIFIED block naming what was checked against swebench 5.0.2 on 2026-08-20 (CLI entrypoint and flags, predictions schema, report filename and the model_name_or_path slash-substitution that does not fire, resolved_ids at harness/reporting.py:154) plus a KNOWN UNFIXED GAP paragraph disclosing that the published eval images are x86_64-only, so an aarch64 daemon silently rebuilds locally and produces numbers not comparable to published ones. The executable change is one defaulted parameter (dataset_name). LOC counts the disclosure at the same rate as code; deleting it would restore the number and re-hide a result-invalidating gap, which non-negotiable 3 forbids. @@ -1077,6 +1122,7 @@ ack verbosity 2b0d39faec60fc08 91 R-R root-relative emission lane: threading the ack verbosity 31105421c3ce88ae 249 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack verbosity 3298be65bf6058ef 1265 cid=7327ba02472bb8c7 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack verbosity 3561d0281d324276 277 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path +ack verbosity 380b7de5df1cfd73 100 cid=5c6aaf66598466c8 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 verbosity 3877dd1e9b4ae997 66 cid=c7ed26e6f4cf641f P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack verbosity 38c814a492cc9c8a 176 cid=a9f0f1c829b158bd by=src/* Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: A2 (dropped_positive, 2026-09-03): emitForLensJson gained the droppedPositiveStanza, mirroring the existing overCeiling/notesStanza envelope-key shape; self-churn is this round's own fresh edit. ack verbosity 3a0425468e6a18cf 157 cid=e6e77cfc3574d18b capture-audit 2026-09-04 wave-2 merge: L10b finding 6 (kHistoryProbeLegend spliced conditionally on res.history, the attributes defined for the first time — historyoraclecheck) + L6 H14 (filter= echoed on the root; the MCP twin's selector parity — mcpattrparitycheck SELECTOR). Two disclosures on one page writer | prior: L10b finding 6: legend clause on --whereis/--doc-drift, conditional on --with-history @@ -1084,7 +1130,7 @@ ack verbosity 3b19cc3d8996c3b2 167 cid=e9c5f629b2a2926a by=src/* lane/n2-i punch ack verbosity 3ba0d3f31d319672 77 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 verbosity 3be1c13661e5a63c 74 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 verbosity 3c07d993bfdbce53 197 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS -ack verbosity 3d87404c1cdf50ec 164 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack verbosity 3d87404c1cdf50ec 180 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack verbosity 3e0a074094789d60 79 cid=3256a8c50529082d Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. ack verbosity 44b41056b999b501 111 D2 audit-regression fixes: renderWholeFiles/chooseExpandServe gain a shaping param each (compress + pack-budget composition, both single-caller, callers updated in the same edit); anchor-row loop and disclosure comments add the LOC/ccx; churn is this lane's own self-churn on the four symbols it edited ack verbosity 45b52ada32f63c11 565 cid=877f2f471ee33bc4 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: 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 @@ -1104,7 +1150,8 @@ ack verbosity 5b224c7fe142bd56 990 cid=6c2e2b906ce06245 by=src/* round ec5e3c3.. ack verbosity 5d8f5288f0df10f7 351 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact | prior: 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 verbosity 5ec38fbd414fa4d4 169 cid=6f20993f1b475898 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: 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 verbosity 6031be13b40a1b6f 205 cid=58496e25cbf67431 round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites -ack verbosity 639de1c3670999f9 183 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack verbosity 6302e2e27e23bcde 64 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack verbosity 639de1c3670999f9 185 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack verbosity 685ade09a168535e 96 cid=124bb29c88050ae1 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | 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 verbosity 6acbcaa854eada23 289 cid=e5c009ae9a0d8393 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack verbosity 7004309695fb79f1 265 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. @@ -1121,6 +1168,7 @@ ack verbosity 7fd074f7cb0b0946 148 cid=0b7c1148e7beac79 lane hb-R3 (harvest-B ca ack verbosity 80a2f3a7c92fbed4 555 cid=df52b38ce1ca6bb9 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4b lane (Rule 2c class-name receiver, 2026-09-03): kParserVer churn=self is the 75->76 bump this round's one ingest fact (Python parameter names as empty-span VarDecl veto evidence) requires; Narrower verbosity 437->481 is the feature — methodOnTypeOrBases hoisted OUT of rule2bFieldRecvType (2b shrank by the same body) and reused by the new rule2cClassNameRecv, each with the WHY comment the house requires. buildGraph's four copies of the narrow-apply loop were folded into one narrowTo lambda in the same change, taking its complexity BELOW the baseline; astropy map and census byte-identical across the fold. ack verbosity 81fbe59b4a35659b 171 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 851e83b4505f10f6 129 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. +ack verbosity 86db2ff4e22cae54 70 cid=b9c9829a2aac834e 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 verbosity 89b3fd14c192c899 1494 cid=bf9296f0f9d8d2a3 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: wave merge 2026-08-20 (harvestexec): TWO seams in buildGraph, both from this wave, measured together at the merged head (cx 753->764, LOC 1327->1360). (a) The RefRole::Type namespace gate, from lane/resolver-precision: +8 cx / +23 LOC, ONE inserted block - a stable in-place filter over the assembled candidate set (12 lines of code) plus the 9-line derivation comment above it, plus a 3-line re-route of the isClassLike lambda through the same predicate. Not extracted to a helper on purpose, and the reason is the gate's own subject matter - on THIS loop the predicate is a provable no-op (the loop admits only role=Call, un-narrowed by resolve.h doctrine, and role=Macro, which retagMacroCallReferences already proves uniquely macro), so the block exists to be the ONE seam a future round edits, and hiding it behind a call would move the lines without removing them while making that edit invisible. test/nsfiltercheck.sh arm 2 is the executable form of the no-op claim and fails if the narrowing ever starts biting here. The measurable effect lives in contextratio.h's all-roles resolution, and the role that reaches it is RefRole::Extends, NOT RefRole::Type - collectFacts continues on Type 28 lines before it calls resolveCandidates, so a Type reference can never arrive there, and what keeps a type mention from spraying across same-named functions is that continue, not this predicate. What the predicate keeps from spraying is a BASE CLAUSE: on test/nsfilterfix, 'class Derived : public Handler' binds to both 'class Handler' and the free 'int Handler( int )' without it (ents 1->2, amb 0->1). Corrected 2026-08-20 by adversarial verification (V-5): the two source comments this ack leaned on named the wrong role, and test/nsfiltercheck.sh arm 5 now pins the real effect on the real role - the gate is red under full removal of the narrowing, which it was not before. Acked separately below. (b) The S5-E compose lang guard, from claude/kind-kepler-0c9b90: +3 cx / +10 LOC, one langCompatible branch plus its rationale comment, so HAS-A resolution applies the same language gate as every other admission site; before it a C++ member bound to a Python/TS same-named class and the duplicate defeated the (ownerSym,fieldName) dedup. Recorded red-first by test/composelangcheck.sh (mixed-lang fixture test/composelangfix, C<->C++ bridge control kept green). Cost is bounded and one-time for both: buildGraph does not grow again when a role or a language is added, only when a seam is. ack verbosity 8a92173ded649e17 158 cid=d49e411bf180293e by=src/* P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack verbosity 8c07caf47e2f33a4 134 cid=0002f98cf93b4127 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. @@ -1140,6 +1188,7 @@ ack verbosity a3a317502ee383bc 77 R-E CORRECTION lane (2026-08-19), the W2-E roo ack verbosity a783c75feea9f253 114 cid=7345e4b8de2407c9 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit ack verbosity a8b774025a21bdc6 338 cid=2fdcb1fc6fa90041 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: P17 slice+edit_check join MCP batch: runBatchSub gains two else-if arms (each calling the standalone builder, sliceText/editCheckText, so a batched answer cannot drift from the live one) plus three argument reads; dispatchMcpLine gains the post_check boolean read for the three edit verbs. Both are dispatch chains growing in kind. runBatchSub is NOT decomposed in this commit on purpose: lane L6 is changing this same function's sub-query GRAMMAR in this wave, and restructuring it here would guarantee a merge conflict over a concurrent lane's work — the decomposition is recorded as a follow-up, not skipped silently. ack verbosity a9f76a08efdb3d50 84 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack verbosity ac9a2be19aa5cd79 653 cid=842f93fca4a9a61f 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 verbosity b496a273ae1564ef 70 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 verbosity b8c5550b5e3dc150 65 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) ack verbosity bb42e692522f57ff 101 cid=aa3e0713df809f6d capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement @@ -1154,9 +1203,9 @@ ack verbosity c840f9a6ec2e6c6b 73 WAVE-2 close (2026-08-19), finding 3 of 3: the ack verbosity c8c7bb0104aa16b8 76 cid=d54042b64ff7b78e H11 (lane ca-L2): writeBaseline gains a DEFAULTED absorbedGating parameter so a dirty pin's absorbed count reaches the sidecar; --edit-check reports incompatible=0 (both existing callers still bind) | prior: 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 verbosity ca97a4b6bf07887b 653 cid=b2c9107cb2d8ac03 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity cb7342964b38db9c 112 cid=3af54969638510bc by=src/* member-variable round (card A3): usesText grows the Owner.field member-selector branch (resolveFieldSelector + bare-name refusal listing spellings), the registered contract of this round | prior: lane/r10-cheap-buckets (r10 GitNexus fix round, LB-A + LB-G). SIX gating rows, ONE lane, read one by one before acking. THREE api-surface contract-changes, all deliberate parameter additions that ARE the feature: (a) mcpverbs usesText 2->3 params, taking McpPageArgs exactly as impactText already did, because the MCP uses verb gained the same default site cap as the CLI and an MCP-only agent that reads capped=1 needs a hatch it can reach (mcpclidiffcheck LENS 1 pins the two surfaces' root-attribute sets equal, so capping one and not the other is a divergence, not a saving); both dispatch sites AND kMcpVerbFields updated in the SAME commit, verified by mcpclidiffcheck/mcpverbscheck/usescheck green. (b)+(c) serialize packSignatures 17->18 and packSignaturesJson 11->12, both taking a trailing hasRelevanceFloor bool, default false so every non---for caller is byte-identical (verified: default map, pack-task, expand, exemplar, recall, hotspots, callers, grep, impact all unchanged). The flag cannot be replaced by passing a smaller topN, because those emitters read topN==0 as ALL, so a query nothing scores on would emit the whole corpus. THREE verbosity rows are the new code itself, already cut twice in this lane: duplicated rule bodies hoisted into relevanceFloorCut/pathTierIndexOver/compareTierThenPath, then the restated rationale moved to those helpers' headers - together taking gating from 13 to 6. What remains is runForLens +10, runCallHierarchy +11 and usesText +14 lines of genuinely new behaviour (the floor cut and its note plumbing; the tier index, the page window and the conditional legend clause). Splitting runForLens is a real refactor of its own - it was 658 lines before this lane touched it - and does not belong in an output-composition fix round. -ack verbosity d42c85b67bd0956f 192 cid=65539b6b8b8297c6 round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) +ack verbosity d42c85b67bd0956f 214 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack verbosity d44595768a7cf3af 106 cid=ad934f87aaaed8df preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. -ack verbosity d63db6944aa504a7 1330 cid=6e21514d6315a8e7 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. +ack verbosity d63db6944aa504a7 1388 cid=1943300f4961480a 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: round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through ack verbosity dbe6ed5269d328a4 325 cid=f7116f48272d6777 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (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 verbosity dd02b378ae6b5b75 126 cid=9ec35c817a609c0f capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost ack verbosity dd627540f10bba76 643 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck @@ -1177,3 +1226,4 @@ ack verbosity f1aa10922e41f258 84 cid=6670a895f0520c6a E3 (terminality round A, ack verbosity f8d747d123b4143d 94 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 verbosity fce48dedc40bc8eb 63 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 verbosity fd4cc5c6c0ad17d9 154 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean +ack verbosity ff2ba74637bbbebf 89 cid=16a96df9744925a7 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. diff --git a/README.md b/README.md index d157ea9f3..b72eb8f4f 100644 --- a/README.md +++ b/README.md @@ -1825,9 +1825,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-587 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +588 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 **587 gate scripts** and is the authoritative list; +`test/regression.sh` names **588 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 7caf24b2c..e4e4ba603 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -1095,7 +1095,7 @@ at: 3954770db+dirty ... [10 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--top-k`, `--mentions`, `--affected`, `--test-gate`, `--legend` +**Shaped by:** `--top-k`, `--mentions`, `--affected`, `--test-gate`, `--legend`, `--limit` ### `--handoff` @@ -3636,7 +3636,7 @@ $ ./build/ripwire . --flags ... [17 more line(s); run it to see the whole thing] ``` -**Shaped by:** `--flip` +**Shaped by:** `--flip`, `--limit` **Caveats (stated by the binary):** @@ -3658,7 +3658,7 @@ $ ./build/ripwire . --flags --flip=RIPWIRE_ASA (empty) ``` -**Shaped by:** `--flags` +**Shaped by:** `--flags`, `--limit` **Caveats (stated by the binary):** @@ -4395,7 +4395,7 @@ $ ./build/ripwire . --hotspots --json **Answers:** paginate a high-cardinality verb paginate a high-cardinality verb. -HONORED by: --deps --callers --callees --tree --lint --hotspots --clones --cochange --owners --communities --community --doc-drift --whereis --grep/--regex --match --pattern --impact --uses --exercises --seams --zoom --external-surface --dead-code --mentions --graph-query --stray-content --test-gate --readability --ensemble --quality-panel --context-ratio --nonlocal-state --comment-coherence --naming-consistency --safe-delete --pr-context --edit-check. Emit at most N rows, skipping the first M; N overrides the verb's own display cap (40 hotspot files, 30 co-change pairs, 60 whereis hits, 100 grep/match hits, 40 impact rows, 20 seam pairs, 40 readability rows, 40 ensemble symbol rows, 40 context-ratio symbol rows, 40 nonlocal-state rows, 200 graph-query rows / --top-k, 40 unflagged --edit-check caller rows). With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0). A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing. Deterministic seams (rows are already sorted) so --offset=N is the exact continuation of the previous --limit=N page. The root element then carries shown= capped= total= has_more= next_offset= offset= limit= — loop until has_more="0". capped= compares the PAGE to the total (1 ⇔ shown < total), so a page past the end reads shown="0" capped="1" has_more="0": nothing was cut, the offset skipped everything — EXCEPT the verbs with TWO INDEPENDENT listings, which carry the noun-prefixed form instead (one shown= could only describe one): --test-gate shown_tests=/tests_capped= + shown_untested=/untested_capped=, --communities shown_modules=/modules_capped= + shown_bridges=/bridges_capped=, --ensemble and --context-ratio shown_syms=/syms_capped= + shown_files=/files_capped=; the window takes the PRIMARY listing (--test-gate's rows; its rows repeat on every page, complete). --edit-check is the same shape for a different reason: its rows split into the ANSWER (callers flagged incompatible="1", with their complete sites_l=) and the CONTEXT (unflagged callers). Only the context pages — shown_unflagged=/unflagged_capped=, with total= the unflagged count — while the flagged rows and the overload census ride every page in full and status=/defs=/callers=/incompatible= are computed over the FULL caller set before any window, so a page can never make the verdict say less than it knows. Any verb NOT in that list REFUSES both flags (exit 1) rather than accepting and ignoring them: budget/top-k verbs (--for/--recall/--pack-task/--from-trace/ --expand/--outline/--pack-signatures/--format=candidates) are shaped by --top-k/--max-tokens/--token-budget, not a page; the rest (--path/--connect/ --around/--exemplar/--report/--mermaid/--map-diff/--metrics and the default map) answer with a single fixed-shape result that has no row list to window at all. +HONORED by: --deps --callers --callees --tree --lint --hotspots --clones --cochange --owners --communities --community --doc-drift --whereis --grep/--regex --match --pattern --impact --uses --exercises --seams --zoom --external-surface --dead-code --mentions --graph-query --stray-content --test-gate --readability --ensemble --quality-panel --context-ratio --nonlocal-state --comment-coherence --naming-consistency --safe-delete --pr-context --edit-check --flags --situ. Emit at most N rows, skipping the first M; N overrides the verb's own display cap (40 hotspot files, 30 co-change pairs, 60 whereis hits, 100 grep/match hits, 40 impact rows, 20 seam pairs, 40 readability rows, 40 ensemble symbol rows, 40 context-ratio symbol rows, 40 nonlocal-state rows, 200 graph-query rows / --top-k, 40 unflagged --edit-check caller rows, 8 --flags read sites per gate, 25 --flip context rows per listing, 8 --situ blast-radius files and 8 co-change partners). A verb NEVER pages the rows that ARE its answer: --edit-check's flagged callers, --flip's and --situ's tests-to-run rows and --flags' gate rows ride every page in full, and every verdict/count attribute is computed over the full set first. With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0). A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing. Deterministic seams (rows are already sorted) so --offset=N is the exact continuation of the previous --limit=N page. The root element then carries shown= capped= total= has_more= next_offset= offset= limit= — loop until has_more="0". capped= compares the PAGE to the total (1 ⇔ shown < total), so a page past the end reads shown="0" capped="1" has_more="0": nothing was cut, the offset skipped everything — EXCEPT the verbs with TWO INDEPENDENT listings, which carry the noun-prefixed form instead (one shown= could only describe one): --test-gate shown_tests=/tests_capped= + shown_untested=/untested_capped=, --communities shown_modules=/modules_capped= + shown_bridges=/bridges_capped=, --ensemble and --context-ratio shown_syms=/syms_capped= + shown_files=/files_capped=; the window takes the PRIMARY listing (--test-gate's rows; its rows repeat on every page, complete). --edit-check is the same shape for a different reason: its rows split into the ANSWER (callers flagged incompatible="1", with their complete sites_l=) and the CONTEXT (unflagged callers). Only the context pages — shown_unflagged=/unflagged_capped=, with total= the unflagged count — while the flagged rows and the overload census ride every page in full and status=/defs=/callers=/incompatible= are computed over the FULL caller set before any window, so a page can never make the verdict say less than it knows. Any verb NOT in that list REFUSES both flags (exit 1) rather than accepting and ignoring them: budget/top-k verbs (--for/--recall/--pack-task/--from-trace/ --expand/--outline/--pack-signatures/--format=candidates) are shaped by --top-k/--max-tokens/--token-budget, not a page; the rest (--path/--connect/ --around/--exemplar/--report/--mermaid/--map-diff/--metrics and the default map) answer with a single fixed-shape result that has no row list to window at all. **Try it** @@ -4425,8 +4425,8 @@ $ ./build/ripwire . --ensemble --limit=8 **Caveats (stated by the binary):** - Emit at most N rows, skipping the first M; +- A verb NEVER pages the rows that ARE its answer: --edit-check's flagged callers, --flip's and --situ's tests-to-run rows and --flags' gate rows ride every page in full, and every verdict/count attribute is computed over the full set first. - With --offset alone (no --limit) the verb's own default page size applies and the root discloses limit="0" — on OUTPUT that 0 means 'no explicit --limit', never a zero-row page (the flag itself refuses --limit=0). -- A BARE run whose default cap cut rows (capped="1") carries the same limit="0" and the whole paging block below, so you can page from the first answer without guessing. ### `--exclude=SUBSTR` diff --git a/docs/EVALS.md b/docs/EVALS.md index d268b8e83..197b63cc5 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` | 587 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 588 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) @@ -5632,7 +5632,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 **587 gate scripts**, all of which exist on disk. +naming **588 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`. @@ -6644,7 +6644,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 587. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 588. 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 8e55c1330..1fc34efc8 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -10,10 +10,10 @@ where the pathological tail is, never near the typical case — and when it fire | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 206 | 81 | 100 | **106** | +| 205 | 81 | 116 | **89** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 206 + 7 is the 213 constants this generator parses out of `src/`. +are not counted as caps, and 205 + 7 is the 212 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -31,8 +31,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. **109 of 206 caps are classified -(37 INDEXING, 37 OUTPUT, 35 BOUNDARY); the remaining 97 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **111 of 205 caps are classified +(37 INDEXING, 39 OUTPUT, 35 BOUNDARY); the remaining 94 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 @@ -127,8 +127,8 @@ Discloses: `bridges_capped`, `files_capped`, `inc_capped`, `modules_capped`, `ro | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kConnectRadiusMax` | `12` | 3093 | — | == connectcfg::kMaxRadius (static_assert at the seam in main.cpp) | -| `kIntFlagMax` | `1000000000` | 3092 | — | parsePosInt/parseNonNegInt's own overflow ceiling | +| `kConnectRadiusMax` | `12` | 3097 | — | == connectcfg::kMaxRadius (static_assert at the seam in main.cpp) | +| `kIntFlagMax` | `1000000000` | 3096 | — | parsePosInt/parseNonNegInt's own overflow ceiling | | `kPageValueMax` | `1000000000` | 591 | — | — | ### `src/cloneidiom.h` @@ -180,13 +180,13 @@ Discloses: **none** ### `src/darkflags.h` -Discloses: **none** +Discloses: `reads_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kMaxAliasDepth` | `8` | 859 | INDEXING | — | -| `kMaxEnvNameLen` | `128` | 58 | BOUNDARY | longest plausible environment-variable name | -| `kMaxSitesShown` | `8` | 57 | OUTPUT | per gate, per list; the rest are counted in a | +| `kMaxAliasDepth` | `8` | 862 | INDEXING | — | +| `kMaxEnvNameLen` | `128` | 61 | BOUNDARY | longest plausible environment-variable name | +| `kMaxSitesShown` | `8` | 60 | OUTPUT | sites per gate; a DEFAULT, raisable by --limit=N (effectiveRowCap), lifted by --detail | ### `src/didyoumean.h` @@ -209,19 +209,19 @@ Discloses: **none** ### `src/docdrift.h` -Discloses: **none** +Discloses: `failed_capped`, `importers_capped`, `weak_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kMaxAnchorsShown` | `12` | 130 | OUTPUT | drifted anchors printed per doc; detail lifts the cap | -| `kMaxClaimedLine` | `200000` | 138 | BOUNDARY | past this a "line number" is a hostile-input example, not a claim | -| `kMaxDecDigits` | `10` | 134 | BOUNDARY | overflow guard on a doc/code integer literal | -| `kMaxExtLen` | `6` | 136 | BOUNDARY | "cpp", "swift", "metal" — longer is not an extension | -| `kMaxFrontMatter` | `12` | 150 | — | — | -| `kMaxHexDigits` | `15` | 135 | BOUNDARY | …hex fits 15 nibbles in 64 bits with room to spare | -| `kMaxNameLen` | `96` | 133 | BOUNDARY | past this it is a sentence, not an identifier | -| `kMinMentionLen` | `4` | 131 | BOUNDARY | a backticked name shorter than this is prose, not code | -| `kMinValueNameLen` | `3` | 132 | BOUNDARY | …and the bar for a `= N` / `[N]` subject name | +| `kMaxAnchorsShown` | `12` | 131 | OUTPUT | failed anchors per doc; a SECONDARY listing (pageview.h rule 6) — --detail lifts it, --limit does not | +| `kMaxClaimedLine` | `200000` | 139 | BOUNDARY | past this a "line number" is a hostile-input example, not a claim | +| `kMaxDecDigits` | `10` | 135 | BOUNDARY | overflow guard on a doc/code integer literal | +| `kMaxExtLen` | `6` | 137 | BOUNDARY | "cpp", "swift", "metal" — longer is not an extension | +| `kMaxFrontMatter` | `12` | 151 | — | — | +| `kMaxHexDigits` | `15` | 136 | BOUNDARY | …hex fits 15 nibbles in 64 bits with room to spare | +| `kMaxNameLen` | `96` | 134 | BOUNDARY | past this it is a sentence, not an identifier | +| `kMinMentionLen` | `4` | 132 | BOUNDARY | a backticked name shorter than this is prose, not code | +| `kMinValueNameLen` | `3` | 133 | BOUNDARY | …and the bar for a `= N` / `[N]` subject name | ### `src/editcheck.h` @@ -290,15 +290,15 @@ Discloses: **none** ### `src/flipimpact.h` -Discloses: **none** +Discloses: `hosts_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kMaxBindings` | `32` | 93 | INDEXING | value-style constants tracked — bounds pass B's needle count | -| `kMaxChainDepth` | `8` | 92 | INDEXING | alias-chain depth cap (mirrors darkflags::kMaxAliasDepth) | -| `kMaxFamily` | `64` | 91 | INDEXING | gates one flip may light — an alias fan-out past this is a table, not a switch | -| `kMaxFlipRows` | `25` | 94 | OUTPUT | per emitted list; --detail lifts every cap | -| `kMaxNearMisses` | `5` | 95 | OUTPUT | "did you mean" suggestions on an unknown gate name | +| `kMaxBindings` | `32` | 96 | INDEXING | value-style constants tracked — bounds pass B's needle count | +| `kMaxChainDepth` | `8` | 95 | INDEXING | alias-chain depth cap (mirrors darkflags::kMaxAliasDepth) | +| `kMaxFamily` | `64` | 94 | INDEXING | gates one flip may light — an alias fan-out past this is a table, not a switch | +| `kMaxFlipRows` | `25` | 97 | OUTPUT | per emitted list; a DEFAULT --limit=N raises and --detail lifts | +| `kMaxNearMisses` | `5` | 98 | OUTPUT | "did you mean" suggestions on an unknown gate name; --limit=N raises it | ### `src/gitmine.h` @@ -527,11 +527,11 @@ Discloses: `coboost_commits_capped`, `hits_capped`, `unindexed_candidates_capped | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kBatchCap` | `16` | 4218 | — | max sub-queries processed per batch; excess is REPORTED, never silently dropped | +| `kBatchCap` | `16` | 4241 | — | max sub-queries processed per batch; excess is REPORTED, never silently dropped | | `kMcpPageValueMax` | `1000000000` | 306 | — | == cli.h's kPageValueMax | | `kMcpRecallTopKMax` | `1000` | 312 | — | — | -| `kOtherDefCap` | `4` | 3953 | OUTPUT | disclosure, not a listing — cap the tail | -| `kRowCap` | `100` | 889 | — | — | +| `kOtherDefCap` | `4` | 3976 | OUTPUT | disclosure, not a listing — cap the tail | +| `kRowCap` | `100` | 902 | — | — | ### `src/mention.h` @@ -609,7 +609,7 @@ Discloses: `count_capped`, `findings_capped`, `hits_capped`, `importers_capped`, | `kCochangePartnerCap` | `30` | 170 | — | — | | `kExternalSurfaceRowCap` | `100` | 186 | — | names, by ref count (≈ 5.2 KB on this repo) | | `kImportReachRowCap` | `40` | 179 | — | — | -| `kPageDisclosureCap` | `224` | 366 | — | — | +| `kPageDisclosureCap` | `224` | 403 | — | — | | `kTreeRowCap` | `80` | 184 | — | files, by best symbol's rank: 80 rows ≈ 11.5 KB on this repo (100 = 14.3 KB) | | `kUseSiteRowCap` | `100` | 165 | — | — | | `kZoomTopModuleCap` | `40` | 185 | — | top-level modules, size desc (their children ride along: levels_shown=2) | @@ -760,11 +760,10 @@ Discloses: `tests_capped`, `untested_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kMaxUntestedRows` | `25` | 939 | — | — | -| `kSituBlastFilesShown` | `8` | 348 | OUTPUT | section [1] — blast-radius file rows | -| `kSituPartnerFileRowsShown` | `4` | 351 | — | section [1] — decl/def partner rows | -| `kSituPartnerRowsShown` | `8` | 350 | — | section [3] — co-change partner rows | -| `kSituTestRowsShown` | `25` | 349 | — | section [2] — tests-to-run rows | +| `kMaxUntestedRows` | `25` | 987 | — | — | +| `kSituBlastFilesShown` | `8` | 355 | OUTPUT | section [1] — blast-radius file rows; a raisable DEFAULT | +| `kSituPartnerFileRowsShown` | `4` | 357 | OUTPUT | section [1] — decl/def partner rows | +| `kSituPartnerRowsShown` | `8` | 356 | OUTPUT | section [3] — co-change partner rows; a raisable DEFAULT | ### `src/skillscan.h` @@ -819,7 +818,7 @@ Discloses: `seed_files_capped` | constant | value | line | class | note | | --- | --- | --- | --- | --- | -| `kRunTraceRelevantLinesCap` | `40` | 696 | — | cap (first/last half split past it) | +| `kRunTraceRelevantLinesCap` | `40` | 698 | — | cap (first/last half split past it) | ### `src/verbs_doctor.h` diff --git a/docs/limits_classes.tsv b/docs/limits_classes.tsv index a6e93df46..a9bb8a436 100644 --- a/docs/limits_classes.tsv +++ b/docs/limits_classes.tsv @@ -112,6 +112,8 @@ kSibliftMaxSib OUTPUT kSideDepthStd INDEXING kSideDepthUses INDEXING kSituBlastFilesShown OUTPUT +kSituPartnerFileRowsShown OUTPUT +kSituPartnerRowsShown OUTPUT kSkillScanFindingCap INDEXING kSliceRdMaxIter OUTPUT kTestHopBasenameRowCap OUTPUT diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index fc5a0220f..606d106f2 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["587 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 + ["588 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."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "587", "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, "588", "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", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["587 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["588 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["46 repos · 69 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 998f5977f..519c250ec 100644 --- a/src/cli.h +++ b/src/cli.h @@ -2364,12 +2364,16 @@ inline constexpr char kHelpTail[] = " --zoom --external-surface --dead-code --mentions --graph-query --stray-content\n" " --test-gate --readability --ensemble --quality-panel --context-ratio\n" " --nonlocal-state --comment-coherence --naming-consistency --safe-delete --pr-context\n" - " --edit-check.\n" + " --edit-check --flags --situ.\n" " Emit at most N rows, skipping the first M; N overrides the verb's own display cap\n" " (40 hotspot files, 30 co-change pairs, 60 whereis hits, 100 grep/match hits, 40\n" " impact rows, 20 seam pairs, 40 readability rows, 40 ensemble symbol rows, 40 context-ratio\n" " symbol rows, 40 nonlocal-state rows, 200 graph-query rows / --top-k, 40\n" - " unflagged --edit-check caller rows).\n" + " unflagged --edit-check caller rows, 8 --flags read sites per gate, 25 --flip\n" + " context rows per listing, 8 --situ blast-radius files and 8 co-change partners).\n" + " A verb NEVER pages the rows that ARE its answer: --edit-check's flagged callers,\n" + " --flip's and --situ's tests-to-run rows and --flags' gate rows ride every page in\n" + " full, and every verdict/count attribute is computed over the full set first.\n" " With --offset alone (no --limit) the verb's own default page size applies and\n" " the root discloses limit=\"0\" — on OUTPUT that 0 means 'no explicit --limit',\n" " never a zero-row page (the flag itself refuses --limit=0). A BARE run whose\n" @@ -3373,12 +3377,18 @@ inline void validatePlanLanes( Config& c ) noexcept // 25-row literal cap (situ.h kMaxUntestedRows) with no shown=/capped= and a refusal on --limit that FALSELY // claimed "no page to walk" (there were 41 more rows). It now windows through pageview.h like every verb // above, so it belongs in the honoring set, not the refusing one. +// 2026-09-10 (C1 F-07/F-10): --flags (with its --flip mode) and --situ join. Both were listing verbs whose +// row caps no flag could reach: --flags cut the sites under a gate at 8, --flip cut six listings at +// 25, and --situ cut its blast-radius and co-change sections at 8 while REFUSING --limit outright. All of +// those listings run through pageWindow() now, which is what membership in this set means. --situ is the +// first PROSE member — it has no XML root, so it spells the same shown=/total=/capped= facts in its section +// headers (situ.h) and test/pagingsweepcheck.sh (L) reads it as prose rather than parsing a root element. constexpr const char* kPagingHonoringVerbs = "--lint --hotspots --callers --callees --tree --deps --cochange --owners --clones --doc-drift " "--communities --community --whereis --grep/--regex --match --pattern --impact --uses --exercises " "--seams --zoom --external-surface --dead-code --mentions --graph-query --stray-content --test-gate " "--readability --ensemble --quality-panel --context-ratio --nonlocal-state --comment-coherence " - "--naming-consistency --safe-delete --pr-context --edit-check"; + "--naming-consistency --safe-delete --pr-context --edit-check --flags --situ"; inline bool honorsPaging( const Config& c ) noexcept { @@ -3390,7 +3400,9 @@ inline bool honorsPaging( const Config& c ) noexcept || !c.graphQuery.empty() || ( c.strayContent && !c.landingPlan && !c.abiFlag ) || c.testGate || c.readability || c.ensemble || c.qualityPanel || c.contextRatio || c.nonlocalState || c.commentCoherence || c.namingConsistency || !c.safeDeleteSym.empty() || c.prContext // P4 (L7): the changed-file window - || !c.editCheckSym.empty(); // 2026-09-10: --edit-check windows its UNFLAGGED caller rows (editcheck.h) + || !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) } // --limit/--offset on a verb that windows NOTHING. Same accept-then-silently-ignore class as every guard in @@ -3643,8 +3655,11 @@ struct ShapingVerb // HONOURS --top-k default map (+ the same riders), --query, --format=candidates, --recall, // --graph-query, and the MCP/batch/--listen pass-throughs // IGNORES both --pack-task, --exemplar, --around, --path, --lego, --report, -// --situ, --scan-skills, --merge-scout, and --for for --top-k (R12's residual) -// (--edit-check LEFT this class on 2026-09-10: it joined honorsPaging when its +// --scan-skills, --merge-scout, and --for for --top-k (R12's residual) +// (--situ and --flags LEFT this class on 2026-09-10 (C1 F-07/F-10), the way +// --edit-check did: their row listings became windowable, so they joined +// honorsPaging and refuse all three like every other member.) +// (--edit-check LEFT this class the same day: it joined honorsPaging when its // unflagged caller rows became windowable, so it refuses all three like every // other paging member instead of accepting them and ignoring them. A verb cannot // hold a row in BOTH tables — the header sentence above is the invariant.) @@ -3673,7 +3688,6 @@ inline constexpr ShapingVerb kShapingVerbs[] = { { "--report", &Config::report, nullptr }, { "--slice", nullptr, &Config::sliceSpec }, { "--at", nullptr, &Config::atSpec }, - { "--situ", &Config::situ, nullptr }, { "--handoff", &Config::handoff, nullptr, false, false, true }, // writeHandoffPacket takes the budget { "--scan-skills", &Config::scanSkills, nullptr }, { "--merge-scout", &Config::mergeScoutFlag, nullptr }, @@ -3690,7 +3704,6 @@ inline constexpr ShapingVerb kShapingVerbs[] = { // which rides the default map's serialize path and honours --top-k/--max-tokens like the map does. { "--html", &Config::html, nullptr, true, true }, { "--verify", nullptr, &Config::verifyClaim }, - { "--flags", &Config::darkFlags, nullptr }, { "--layout", &Config::layoutFlag, nullptr }, { "--field-affinity", &Config::fieldAffinity, nullptr }, { "--naming-calibration",&Config::namingCalibration, nullptr }, diff --git a/src/darkflags.h b/src/darkflags.h index c9d559d63..81bb70360 100644 --- a/src/darkflags.h +++ b/src/darkflags.h @@ -35,12 +35,15 @@ #include "arch.h" // relForHash #include "serialize.h" // escapeXml #include "docparse.h" // lowerExtOf / isDocExtension — which files are PROSE, not code +#include "pageview.h" // §P8: pageWindow / effectiveRowCap / secondaryCutAttrs — the ONE paging contract +#include "nextverb.h" // P3: nextAttrXml — the ONE pasteable follow-up a cut root carries #include "infra/Diagnostics.h" // DEGRADED_PATH_ALERT #include "btree.hpp" // gtl::btree_map — sorted iteration (house rule: never std::map) #include #include +#include #include #include #include @@ -54,7 +57,7 @@ namespace darkflags { constexpr std::size_t kMaxFlagFileBytes = 4u << 20; // 4 MB — past this a "source" file is generated data -constexpr std::size_t kMaxSitesShown = 8; // per gate, per list; the rest are counted in a +constexpr std::size_t kMaxSitesShown = 8; // sites per gate; a DEFAULT, raisable by --limit=N (effectiveRowCap), lifted by --detail constexpr std::size_t kMaxEnvNameLen = 128; // longest plausible environment-variable name enum class GateKind : std::uint8_t { Compile = 0, CMake, Env }; @@ -1058,11 +1061,27 @@ inline FlagsResult computeFlags( const IngestResult& ing, const std::string& roo using XmlEscaper = std::function; -inline void writeGate( std::FILE* out, const Gate& g, const XmlEscaper& ex, std::size_t maxSites ) +// C1 F-07 (2026-09-10): the site list was cut at 8 and this file emitted no shown=/total=/capped= +// token anywhere — a reporting verb dropping rows in silence, with only the remainder to say +// so and no flag that could lift it. The GATE rows are the answer here ("what is built but dark") and are +// never windowed; the SITES under one gate are context, so they are what pages. reads= on the same element +// is already this listing's rule-2 total, so the disclosure is rule 1's pair alone (pageview.h). +inline std::size_t gateReadWindow( const Gate& g, std::size_t maxSites, int pageOffset, PageWindow& window ) noexcept { - rw::emitTo( out, "", + // maxSites == SIZE_MAX is --detail ("every row"), which pageWindow spells as limit <= 0. + const int limit = maxSites >= std::size_t( INT_MAX ) ? 0 : int( maxSites ); + window = pageWindow( g.reads.size(), limit, pageOffset ); + return window.end - window.begin; +} + +inline void writeGate( std::FILE* out, const Gate& g, const XmlEscaper& ex, std::size_t maxSites, int pageOffset = 0 ) +{ + PageWindow readPage{ 0, 0 }; + const std::size_t shownCount = gateReadWindow( g, maxSites, pageOffset, readPage ); + rw::emitTo( out, "", ex( g.name ).c_str(), gateKindTag( g.kind ), ex( g.def ).c_str(), isDarkDefault( g.def ) ? 1 : 0, - g.regions, g.guardedLines, g.reads.size(), ex( g.defSite.path ).c_str(), g.defSite.line ); + g.regions, g.guardedLines, g.reads.size(), ex( g.defSite.path ).c_str(), g.defSite.line, + secondaryCutAttrs( "reads", shownCount, g.reads.size() ).c_str() ); if( !g.aliasOf.empty() ) { rw::emitTo( out, "", ex( g.aliasOf ).c_str() ); @@ -1080,8 +1099,7 @@ inline void writeGate( std::FILE* out, const Gate& g, const XmlEscaper& ex, std: // is exactly what it will not. The `shown++ >= cap` form got this wrong twice over — it left the counter // at cap+1, so under-reported the drop by one, and at exactly cap+1 reads the element vanished // entirely and one row disappeared unmarked. abicheck.h::writeAbiRef is the shape this follows. - const std::size_t shownCount = std::min( g.reads.size(), maxSites ); - for( std::size_t readIndex = 0; readIndex < shownCount; ++readIndex ) + for( std::size_t readIndex = readPage.begin; readIndex < readPage.end; ++readIndex ) { rw::emitTo( out, "", ex( g.reads[ readIndex ].path ).c_str(), g.reads[ readIndex ].line ); } @@ -1092,7 +1110,7 @@ inline void writeGate( std::FILE* out, const Gate& g, const XmlEscaper& ex, std: rw::emitRaw( out, "" ); } -inline void writeFlags( std::FILE* out, const FlagsResult& res, std::size_t maxSites ) +inline void writeFlags( std::FILE* out, const FlagsResult& res, std::size_t maxSites, int pageOffset = 0 ) { std::vector esc; const XmlEscaper ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; @@ -1106,6 +1124,20 @@ inline void writeFlags( std::FILE* out, const FlagsResult& res, std::size_t maxS "is the COUNT of dark gates; it was spelled dark until that collided with the child bool. files= is THIS " "verb's own harvest scan (source + CMakeLists files it read looking for gates) — a wider crawl than the " "map's indexed corpus, so it will not equal the map's files= -->" ); + // C1 F-07: the site listing's own vocabulary, DEFINED where the reader meets it (legendcoveragecheck's + // rule). Emitted unconditionally because it describes a listing every run carries, unlike the attributes + // themselves, which appear only on a gate that was actually cut. + rw::emitRaw( out, "" ); // §P8 collision: `dark=` was a COUNT here and a BOOL on the children beneath — indistinguishable // to a parser. The count is renamed (index-vs-count rule) and reads correctly beside its // gates=/compile=/cmake=/env= siblings; it had ZERO parsers, so the bool half keeps its name. @@ -1114,12 +1146,25 @@ inline void writeFlags( std::FILE* out, const FlagsResult& res, std::size_t maxS std::vector fgEsc; const std::string fgFilterAttr = res.filter.empty() ? std::string() : ( " filter=\"" + std::string( rw::escapeXml( res.filter, fgEsc ) ) + "\"" ); - rw::emitTo( out, "", + // P3 (nextverb.h): the ONE pasteable follow-up, and it is EXACT — the smallest --limit that cuts no + // gate's site list. Empty when nothing was cut, so an uncut root is byte-identical to what it was. + std::size_t widestCut = 0; + for( const Gate& g : res.gates ) + { + PageWindow probe{ 0, 0 }; + if( gateReadWindow( g, maxSites, pageOffset, probe ) < g.reads.size() ) + { + widestCut = std::max( widestCut, g.reads.size() ); + } + } + const std::string flagsNext = widestCut == 0 ? std::string() + : nextAttrXml( "--flags --limit=" + std::to_string( widestCut ) ); + rw::emitTo( out, "", res.gates.size(), res.dark, res.compileCount, res.cmakeCount, res.envCount, res.filesScanned, - fgFilterAttr.c_str() ); + fgFilterAttr.c_str(), flagsNext.c_str() ); for( const Gate& g : res.gates ) { - writeGate( out, g, ex, maxSites ); + writeGate( out, g, ex, maxSites, pageOffset ); } rw::emitRaw( out, "" ); } diff --git a/src/docdrift.h b/src/docdrift.h index 9dca8e674..05a67517c 100644 --- a/src/docdrift.h +++ b/src/docdrift.h @@ -106,6 +106,7 @@ #include "infra/Diagnostics.h" // VERIFY / DEGRADED_PATH_ALERT #include "gitstamp.h" // r26-stamp Task A: gitstamp::stampAt — the at="[+dirty]" root anchor #include "layout.h" // layout::isCFamilyPath — shared C/C++/ObjC/CUDA extension classifier +#include "nextverb.h" // P3: nextAttrXml — the ONE pasteable follow-up a cut root carries #include #include @@ -127,7 +128,7 @@ namespace docdrift // ── tuning constants (every bound the report rests on, in one place) ───────────────────────────────────── -constexpr std::size_t kMaxAnchorsShown = 12; // drifted anchors printed per doc; detail lifts the cap +constexpr std::size_t kMaxAnchorsShown = 12; // failed anchors per doc; a SECONDARY listing (pageview.h rule 6) — --detail lifts it, --limit does not constexpr std::size_t kMinMentionLen = 4; // a backticked name shorter than this is prose, not code constexpr std::size_t kMinValueNameLen = 3; // …and the bar for a `= N` / `[N]` subject name constexpr std::size_t kMaxNameLen = 96; // past this it is a sentence, not an identifier @@ -2651,7 +2652,27 @@ inline constexpr const char* kDocDriftLegend = "Neither number is " "wrong. corpus=\"0\" means the corpus scan never ran at all, which happens only when the docs raised " "no anchor SHAPE whatsoever — prose ones included — so anchors=\"0\" beside a non-zero prose= still " - "scanned, and still reports the corpus it scanned. "; + "scanned, and still reports the corpus it scanned. " + // 2026-09-10 (listing-paging round, C1 F-06): the per-doc listing was cut at 12 with no shown=/capped= + // anywhere and no way to lift it — the verb sits in the --limit-honoring set, but --limit windowed the + // rows only, so --limit=1000000 still served 12 anchors a doc. See pageview.h, THE TRUNCATION + // VOCABULARY (rules 1, 3 and 6) for the pair; this paragraph DEFINES it where the reader meets it. + "PER-DOC ROW LISTINGS AND WHAT THEY DISCLOSE. The rows under a are the FAILED anchors of that " + "doc and they are capped, by default at 12 a doc. A doc whose listing was cut says so on its own element: " + "shown_failed= is how many rows this run printed, failed_capped=\"1\" says rows were dropped, and " + "failed_total= is the whole failed population for that doc — which is drift= + dated=, stated as its own " + "number so no reader has to sum two attributes to learn what was cut, and deliberately NOT spelled " + "anchors_total=, because anchors= on the same element counts EVERY anchor in the doc and not these rows. " + "The groups cap the same way and disclose the same way against their own n=: " + "shown_weak= with weak_capped=\"1\". Both pairs are emitted ONLY when the cut happened — no element " + "carries a capped=\"0\" that could never fire — and the more drift= / more weak= remainders they sit " + "beside are unchanged. THE VERDICT IS NEVER THE WINDOW: docs=, clean=, anchors=, checked=, unchecked=, " + "drift=, dated= and prose= on this root, and drift=/dated=/anchors=/checked= on every , are computed " + "over the FULL anchor set before any cap exists, so raising or removing the cap cannot move one of them. " + "limit=N does NOT raise this cap and is not meant to: it windows the ROWS, which are this " + "report's primary listing, and one flag governing both would make the same doc print a different number " + "of anchor rows depending on the page it was served on. The flag that lifts the per-doc cap entirely is " + "detail=N (any N above zero), and next= on this root is exactly that invocation, emitted only when a listing was cut. "; // §L10b: the trailing "-->" moved to this constant's ONE call site (below), which now splices in // gitoracle::kHistoryProbeLegend first — the with_history lane's own element, previously // undefined on this legend, shared verbatim with --whereis's copy so the two cannot drift. @@ -2667,8 +2688,10 @@ inline void writeWeakDisclosures( std::FILE* out, const DriftResult& res, std::s { for( const WeakDocGroup& g : res.weakGroups ) { - rw::emitTo( out, "", ex( g.path ).c_str(), g.rows.size() ); const std::size_t shownCount = std::min( g.rows.size(), maxPerDoc ); + // n= is already this listing's rule-2 total, so the pair rides without a third number. + rw::emitTo( out, "", ex( g.path ).c_str(), g.rows.size(), + secondaryCutAttrs( "weak", shownCount, g.rows.size() ).c_str() ); for( std::size_t rowIndex = 0; rowIndex < shownCount; ++rowIndex ) { writeWeakAnchor( out, g.rows[ rowIndex ], ex ); @@ -2681,6 +2704,46 @@ inline void writeWeakDisclosures( std::FILE* out, const DriftResult& res, std::s } } +// P3 (nextverb.h): the ONE pasteable follow-up, and it is EXACT — --detail is the flag that lifts the +// per-doc cap outright, so the invocation it names cuts nothing at all rather than being sized to this run. +// Empty when NO listing was cut, which is what keeps an uncut root byte-identical to what it was. Its own +// function rather than a prepass inside the emitter: two scans and a predicate are a fact about the +// document, and folding them into writeDocDriftPage took that function from ccx 12 to 19, past the bar. +inline std::string docDriftNextAttr( const DriftResult& res, const PageWindow& docPage, std::size_t anchorCap ) +{ + bool cut = false; + for( std::size_t docIndex = docPage.begin; docIndex < docPage.end && !cut; ++docIndex ) + { + cut = res.docs[ docIndex ].drifted.size() > anchorCap; + } + for( std::size_t groupIndex = 0; groupIndex < res.weakGroups.size() && !cut; ++groupIndex ) + { + cut = res.weakGroups[ groupIndex ].rows.size() > anchorCap; + } + return cut ? nextAttrXml( "--doc-drift --detail=1" ) : std::string(); +} + +// C1 F-06 (2026-09-10). The finding: --doc-drift sits in cli.h's honorsPaging set, but --limit reached +// the rows ONLY, so `--doc-drift --limit=1000000` served the same 56 anchor rows the bare run did, +// with the cut disclosed by nothing but a remainder. The suggested fix was to route the +// per-doc cap through effectiveRowCap like every PRIMARY row cap in the tool. +// +// THAT FIX WAS BUILT, AND IT BREAKS THE PAGING CONTRACT — measured, not reasoned about. The rows are +// a SECONDARY listing (pageview.h rule 6: the paging half describes the report's PRIMARY, --limit +// windowed listing, and the primary here is the rows). Tie the secondary cap to the SAME --limit +// and the element's own text starts varying with the window: `--limit=3` prints shown_failed="3" +// and `--limit=6` prints shown_failed="6" for the same doc, so page[0:3] + page[3:6] no longer equals +// page[0:6] and test/pagingsweepcheck.sh's --offset continuity arm goes red on doc-drift — correctly. +// A paged walk that is not equivalent to the whole is the §P8 bug this family exists to prevent. +// +// So this follows the precedent the tool already set for exactly this shape: kImportReachRowCap, the +// secondary import tier under --impact, which pageview.h states "is NOT raisable by --limit — rule 6 +// reserves the paging half for the PRIMARY listing, so a secondary one discloses through +// shown_importers=/importers_capped= and nothing else." Same here. What F-06 is actually about is the +// SILENCE, and that is what closes: the cut says shown_failed=/failed_capped=/failed_total= on the doc +// it happened to, and the root names the exact invocation that lifts it — --detail, which has lifted +// this cap since the verb was written and which no output ever mentioned. +// // §P8: --limit/--offset used to be accepted and IGNORED here — every run emitted the same full list, // so a paging loop over --doc-drift never advanced. `pageLimit`/`pageOffset` (0 = un-paginated, the pre-§P8 // shape byte for byte) window the ROWS, which are already deterministically ordered (§P11.10: live @@ -2705,6 +2768,10 @@ inline void writeDocDriftPage( std::FILE* out, const DriftResult& res, std::size const PageWindow docPage = pageWindow( res.docs.size(), pageLimit, pageOffset ); + const std::size_t anchorCap = maxPerDoc; // C1 F-06: --detail lifts it, --limit deliberately does not (see above) + + const std::string docDriftNext = docDriftNextAttr( res, docPage, anchorCap ); + std::fputs( kDocDriftLegend, out ); // §L10b: the clause only when --with-history actually made that element reachable — an // unconditional splice would cost every plain --doc-drift run bytes describing an absent element. @@ -2734,6 +2801,7 @@ inline void writeDocDriftPage( std::FILE* out, const DriftResult& res, std::size rw::emitTo( out, "{}", pageDisclosure( pab, sizeof( pab ), docPage.end - docPage.begin, res.docs.size(), docPage.end, pageLimit, pageOffset, false ) ); } + rw::emitTo( out, "{}", docDriftNext.c_str() ); rw::emitRaw( out, ">" ); // What the history probe did, when it was asked for — stated up front so a reader knows whether the @@ -2746,14 +2814,19 @@ inline void writeDocDriftPage( std::FILE* out, const DriftResult& res, std::size for( std::size_t docIndex = docPage.begin; docIndex < docPage.end; ++docIndex ) { const DocRow& row = res.docs[ docIndex ]; - rw::emitTo( out, "", + // THE VERDICT IS COMPUTED FROM THE FULL SET, NOT THE WINDOW: both numbers below are taken from + // row.drifted.size() / row.datedCount, which the cap never touches. Asserted rather than trusted — + // the whole class this round closes is a count that quietly starts following the emitted rows. + VERIFY( row.datedCount <= row.drifted.size() ); + rw::emitTo( out, " // remainder is exactly what it will not. The `shown++ >= cap` form got this wrong twice over — it // left the counter at cap+1, so under-reported the drop by one, and at exactly cap+1 rows // the element vanished entirely and one row disappeared unmarked. - const std::size_t shownCount = std::min( row.drifted.size(), maxPerDoc ); + const std::size_t shownCount = std::min( row.drifted.size(), anchorCap ); + rw::emitTo( out, "{}>", secondaryCutAttrs( "failed", shownCount, row.drifted.size(), "failed_total" ).c_str() ); for( std::size_t anchorIndex = 0; anchorIndex < shownCount; ++anchorIndex ) { writeAnchor( out, row.drifted[ anchorIndex ], ex ); @@ -2765,7 +2838,7 @@ inline void writeDocDriftPage( std::FILE* out, const DriftResult& res, std::size rw::emitRaw( out, "" ); } - writeWeakDisclosures( out, res, maxPerDoc, ex ); + writeWeakDisclosures( out, res, anchorCap, ex ); // The two tallies print the same shape from two tables, so one emitter serves both — the reader can see // WHICH reason or WHICH dating mark carried each count rather than taking the header number on trust. diff --git a/src/flipimpact.h b/src/flipimpact.h index 51fd6138c..942322a9a 100644 --- a/src/flipimpact.h +++ b/src/flipimpact.h @@ -70,6 +70,8 @@ #include "docparse.h" // isProseExtension / lowerExtOf — the shared prose vocabulary #include "serialize.h" // escapeXml #include "testmap.h" // M21(b): TestRunnerIndex / runAttrDisclosed — the ONE run= hint the tests_to_run family shares +#include "pageview.h" // §P8: pageWindow / effectiveRowCap / secondaryCutAttrs — the ONE paging contract +#include "nextverb.h" // P3: nextAttrXml / kNextAttrMaxBytes — the ONE pasteable follow-up #include "infra/Diagnostics.h" // DEGRADED_PATH_ALERT #include "btree.hpp" // gtl::btree_map — sorted iteration (house rule: never std::map) @@ -77,6 +79,7 @@ #include #include #include +#include #include #include #include @@ -91,8 +94,8 @@ namespace flipimpact constexpr std::size_t kMaxFamily = 64; // gates one flip may light — an alias fan-out past this is a table, not a switch constexpr std::uint32_t kMaxChainDepth = 8; // alias-chain depth cap (mirrors darkflags::kMaxAliasDepth) constexpr std::size_t kMaxBindings = 32; // value-style constants tracked — bounds pass B's needle count -constexpr std::size_t kMaxFlipRows = 25; // per emitted list; --detail lifts every cap -constexpr std::size_t kMaxNearMisses = 5; // "did you mean" suggestions on an unknown gate name +constexpr std::size_t kMaxFlipRows = 25; // per emitted list; a DEFAULT --limit=N raises and --detail lifts +constexpr std::size_t kMaxNearMisses = 5; // "did you mean" suggestions on an unknown gate name; --limit=N raises it // ── result model (POD-ish, ids/handles over pointers) ──────────────────────────────────────────────────── @@ -142,6 +145,7 @@ struct FlipResult bool ok = false; // false ⇒ refuse loudly; never emit an empty-looking success bool unknownGate = false; std::vector nearMisses; // cheap suggestions for an unknown name + std::size_t nearMissTotal = 0; // C1 F-07: how many QUALIFIED before the cap above cut the list // the flipped gate's own identity, copied from the harvest (never recomputed) std::string name; @@ -651,7 +655,11 @@ inline ValueScanResult scanValueLane( const IngestResult& ing, const std::string // Gate names are SCREAMING_SNAKE and usually family-prefixed, so the useful hint is containment // (`RRF_ALL` → `CANYON_RRF_ALL`), with a shared-prefix score as the fallback. (main.cpp's didYouMean scores // the SYMBOL pool for typo'd function names — a different pool answering a different question.) -inline std::vector nearestGateNames( const std::vector& gates, std::string_view want ) +// C1 F-07: `maxOut` is a raisable DEFAULT (--limit=N through effectiveRowCap at the call site), and +// `totalOut` reports how many candidates QUALIFIED — a "did you mean" list that silently dropped seven +// better names is the same silent cut as a report row cap, on the one output a lost caller reads. +inline std::vector nearestGateNames( const std::vector& gates, std::string_view want, + std::size_t maxOut = kMaxNearMisses, std::size_t* totalOut = nullptr ) { const auto lower = []( std::string_view v ) { std::string o; o.reserve( v.size() ); for( char c : v ) { o.push_back( char( std::tolower( (unsigned char)c ) ) ); } return o; }; const std::string wantLow = lower( want ); @@ -687,9 +695,13 @@ inline std::vector nearestGateNames( const std::vector kMaxNearMisses ) + if( totalOut != nullptr ) { - cands.resize( kMaxNearMisses ); + *totalOut = cands.size(); + } + if( cands.size() > maxOut ) + { + cands.resize( maxOut ); } std::vector out; @@ -962,7 +974,8 @@ inline void adoptGateIdentity( const gtl::btree_map& excludes, std::string_view gateName ) + const std::vector& excludes, std::string_view gateName, + int pageLimit = 0 ) { FlipResult res; @@ -981,7 +994,8 @@ inline FlipResult computeFlip( const IngestResult& ing, const Graph& g, const st if( self == byName.end() ) { res.unknownGate = true; - res.nearMisses = nearestGateNames( harvest.gates, gateName ); + res.nearMisses = nearestGateNames( harvest.gates, gateName, + std::size_t( effectiveRowCap( pageLimit, int( kMaxNearMisses ) ) ), &res.nearMissTotal ); return res; // ok stays false — the caller refuses loudly } const darkflags::Gate& gate = self->second; @@ -1044,18 +1058,32 @@ inline std::string qualifiedName( const Symbol& s ) // Rows + the honest `` remainder, WITHOUT a wrapper element (the lights block holds two of these). // Returns how many it printed. Every capped list in the report goes through here, so "cap then admit what // was elided" is written once instead of six times. +// C1 F-07 (2026-09-10): every list below was cut at 25 and this file emitted no shown=/total=/capped= token +// anywhere — the `` remainder was the whole disclosure, and no flag could lift the cap. The window +// is pageview.h's now, so --limit=N raises it and --offset=M pages it exactly as it does everywhere else, +// and the cut is disclosed in the shared vocabulary. `maxRows == SIZE_MAX` is --detail ("every row"), which +// pageWindow spells as limit <= 0. +inline PageWindow flipRowWindow( std::size_t total, std::size_t maxRows, int pageOffset ) noexcept +{ + const int limit = maxRows >= std::size_t( INT_MAX ) ? 0 : int( maxRows ); + return pageWindow( total, limit, pageOffset ); +} + template -inline std::size_t writeCappedRows( std::FILE* out, const char* moreAttr, const Seq& seq, std::size_t maxRows, Row&& row ) +inline std::size_t writeCappedRows( std::FILE* out, const char* moreAttr, const Seq& seq, std::size_t maxRows, Row&& row, + int pageOffset = 0 ) { - std::size_t shown = 0; + const PageWindow window = flipRowWindow( seq.size(), maxRows, pageOffset ); + std::size_t index = 0; + std::size_t shown = 0; for( const auto& item : seq ) { - if( shown >= maxRows ) + if( index >= window.begin && index < window.end ) { - break; + ++shown; + row( item ); } - ++shown; - row( item ); + ++index; } if( seq.size() > shown ) { @@ -1064,18 +1092,54 @@ inline std::size_t writeCappedRows( std::FILE* out, const char* moreAttr, const return shown; } -// The same, wrapped in ``. +// The same, wrapped in `` — n= is the listing's rule-2 total, so a cut adds rule 1's +// pair beside it (shown_= / _capped="1") and nothing more. template -inline void writeCappedList( std::FILE* out, const char* tag, const Seq& seq, std::size_t maxRows, Row&& row ) +inline void writeCappedList( std::FILE* out, const char* tag, const Seq& seq, std::size_t maxRows, Row&& row, + int pageOffset = 0 ) { - rw::emitTo( out, "<{} n=\"{}\">", tag, seq.size() ); - writeCappedRows( out, tag, seq, maxRows, row ); + const PageWindow window = flipRowWindow( seq.size(), maxRows, pageOffset ); + rw::emitTo( out, "<{} n=\"{}\"{}>", tag, seq.size(), + secondaryCutAttrs( tag, window.end - window.begin, seq.size() ).c_str() ); + writeCappedRows( out, tag, seq, maxRows, row, pageOffset ); rw::emitTo( out, "", tag ); } +// P3 (nextverb.h): the ONE pasteable follow-up, and it is EXACT — the smallest --limit that cuts none of +// the listings THIS run cut. Empty when nothing was cut (so an uncut root is byte-identical to what it was) +// and empty again if a very long gate name pushes the invocation past kNextAttrMaxBytes, because a truncated +// command line is worse than none — the reader still has the cap disclosure on every cut listing. +inline std::string flipNextInvocation( const FlipResult& res, std::size_t maxRows, int pageOffset ) +{ + const std::size_t totals[] = { res.regions.size(), res.branches.size(), res.hosts.size(), + res.downstream.size(), res.untested.size(), res.buildSites.size() }; + std::size_t widestCut = 0; + for( const std::size_t total : totals ) + { + const PageWindow window = flipRowWindow( total, maxRows, pageOffset ); + if( window.end - window.begin < total ) + { + widestCut = std::max( widestCut, total ); + } + } + if( widestCut == 0 ) + { + return {}; + } + const std::string invocation = "--flags " + rw::nextFlag( "--flip=", res.name ) + " --limit=" + std::to_string( widestCut ); + return invocation.size() > rw::kNextAttrMaxBytes ? std::string() : invocation; +} + +// C1 F-07 (2026-09-10): six row listings, every one cut at 25 in silence. The vocabulary they use is +// DEFINED here, where the reader meets it (legendcoveragecheck's rule), and as its own constant rather +// than 15 more lines inside writeFlipHeader, which the verbosity bar counts and is right to. +inline constexpr const char* kFlipRowLegend = + "ROWS AND WHAT IS NEVER CUT: the t rows are the tests_to_run answer and are never windowed, capped or paged, exactly as the test gate verb serves its own — a listing you act on is not a listing that may be trimmed. Every other listing here is CONTEXT and pages at 25 rows by default: r and b inside lights, hosts, downstream, untested and the build sites. A listing that was cut says so on its own wrapper, against the n= (or r=/b=) total already there: shown_hosts= with hosts_capped=\"1\", and the same pair under shown_downstream=, shown_untested=, shown_r=, shown_b= and shown_build=. The pair rides ONLY a listing that was actually cut, never as a capped=\"0\" on one that fit, and the more rows= remainder beside it is unchanged. THE VERDICT IS NEVER THE WINDOW: family/regions/loc/branches/bindings/hosts/filescope/downstream/dependents/tests/untested/files on this root are counted over the FULL sets before any cap exists, so raising or removing a cap cannot move one of them. limit=N raises every context cap (offset=M pages them), detail lifts them all, and next= is the exact pasteable invocation that shows every row this run dropped. "; + // The doc comment, the `` header attributes, and the four situational rows that qualify them // (already-lit / also / parent / capped) plus the family roll-up. -inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex ) +inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEscaper& ex, + const std::string& nextInvocation = std::string() ) { rw::emitTo( out, "", + "three numbers count three different things and must never be compared or summed across verbs. {}-->", // M21(b): the run=/run_unknown= rule, from testmap.h's ONE constant. - std::string( rw::kRunHintLegendClause ).c_str() ); + std::string( rw::kRunHintLegendClause ).c_str(), kFlipRowLegend ); rw::emitTo( out, "", + " hosts=\"{}\" filescope=\"{}\" downstream=\"{}\" dependents=\"{}\" tests=\"{}\" untested=\"{}\" files=\"{}\"{}>", ex( res.name ).c_str(), darkflags::gateKindTag( res.kind ), ex( res.def ).c_str(), res.isDark ? 1 : 0, res.isRuntime ? 1 : 0, ex( res.defSite.path ).c_str(), res.defSite.line, res.family.size(), res.totalRegions, res.totalLines, res.branches.size(), res.bindings.size(), res.hosts.size(), res.fileScopeLights, res.downstream.size(), res.dependents, - res.tests.size(), res.untested.size(), res.filesScanned ); + res.tests.size(), res.untested.size(), res.filesScanned, + rw::nextAttrXml( nextInvocation ).c_str() ); // the contradiction row: this gate is ALREADY lit by the winning declaration, and dark only in the other if( !res.isDark ) @@ -1134,33 +1199,39 @@ inline void writeFlipHeader( std::FILE* out, const FlipResult& res, const XmlEsc // The two lit-site row kinds, in one element: `#if` regions and C++ branch sites. inline void writeFlipLights( std::FILE* out, const FlipResult& res, const IngestResult& ing, - const XmlEscaper& ex, std::size_t maxRows ) + const XmlEscaper& ex, std::size_t maxRows, int pageOffset = 0 ) { - rw::emitTo( out, "", res.regions.size(), res.branches.size() ); + // TWO independent listings on ONE element, so two noun-prefixed pairs (pageview.h rule 1) against the + // r=/b= totals already here — never a bare shown=, which could only describe one of them. + const PageWindow regionPage = flipRowWindow( res.regions.size(), maxRows, pageOffset ); + const PageWindow branchPage = flipRowWindow( res.branches.size(), maxRows, pageOffset ); + rw::emitTo( out, "", res.regions.size(), res.branches.size(), + secondaryCutAttrs( "r", regionPage.end - regionPage.begin, res.regions.size() ).c_str(), + secondaryCutAttrs( "b", branchPage.end - branchPage.begin, res.branches.size() ).c_str() ); writeCappedRows( out, "r", res.regions, maxRows, [ & ]( const LitRegion& r ) { rw::emitTo( out, "", ex( r.path ).c_str(), r.line, r.lines, ex( r.gate ).c_str(), r.hostCount ); - } ); + }, pageOffset ); writeCappedRows( out, "b", res.branches, maxRows, [ & ]( const LitBranch& b ) { rw::emitTo( out, "", ex( b.path ).c_str(), b.line, ex( b.gate ).c_str(), ex( b.via ).c_str(), b.host == kNoNode ? "" : ex( ing.symbols[ b.host ].name ).c_str() ); - } ); + }, pageOffset ); rw::emitRaw( out, "" ); } inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult& ing, - const std::string& root, std::size_t maxRows ) + const std::string& root, std::size_t maxRows, int pageOffset = 0 ) { std::vector esc; const XmlEscaper ex = [ & ]( std::string_view s ) { return std::string( escapeXml( s, esc ) ); }; const auto rel = [ & ]( std::uint32_t fileId ) { return std::string( relForHash( ing.files[ fileId ], root ) ); }; const auto isTested = [ & ]( NodeId n ) { return n < res.testReach.size() && res.testReach[n]; }; - writeFlipHeader( out, res, ex ); - writeFlipLights( out, res, ing, ex, maxRows ); + writeFlipHeader( out, res, ex, flipNextInvocation( res, maxRows, pageOffset ) ); + writeFlipLights( out, res, ing, ex, maxRows, pageOffset ); for( const ValueBinding& b : res.bindings ) { @@ -1173,19 +1244,23 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult const Symbol& s = ing.symbols[h]; rw::emitTo( out, "", ex( qualifiedName( s ) ).c_str(), ex( rel( s.fileId ) ).c_str(), s.line, s.ccx, isTested( h ) ? 1 : 0 ); - } ); + }, pageOffset ); writeCappedList( out, "downstream", res.downstream, maxRows, [ & ]( NodeId d ) { const Symbol& s = ing.symbols[d]; rw::emitTo( out, "", ex( qualifiedName( s ) ).c_str(), ex( rel( s.fileId ) ).c_str(), s.ccx ); - } ); + }, pageOffset ); // M21(b) (capture-audit 2026-09-04): this listing is a tests_to_run row family like every other, // and it asked for no runner at all — so a reader of a flip report could not tell a harness with no // derivable command from one this verb never looked up. Lazy by construction: a flip with no test row // never reads a runner script. + // C1 F-07 (2026-09-10): the t rows are the ANSWER, and they used to page like everything else — a flip + // with 26 reachable tests named 25 of them and dropped the 26th because it sorted last. --test-gate's + // own listing has never been windowed for exactly this reason; this listing is the same obligation + // read from a different seed, so it is served whole on every page. SIZE_MAX, not maxRows. const rw::TestRunnerIndex flipRunners( ing ); - writeCappedList( out, "tests", res.tests, maxRows, [ & ]( std::uint32_t f ) + writeCappedList( out, "tests", res.tests, SIZE_MAX, [ & ]( std::uint32_t f ) { rw::emitTo( out, "", ex( rel( f ) ).c_str(), rw::runAttrDisclosed( flipRunners, f, ex ).c_str() ); } ); @@ -1194,16 +1269,18 @@ inline void writeFlip( std::FILE* out, const FlipResult& res, const IngestResult const Symbol& s = ing.symbols[u]; rw::emitTo( out, "", ex( qualifiedName( s ) ).c_str(), ex( rel( s.fileId ) ).c_str(), s.line, s.ccx ); - } ); + }, pageOffset ); if( !res.buildSites.empty() ) { - rw::emitTo( out, "", - res.buildSites.size() ); + const PageWindow buildPage = flipRowWindow( res.buildSites.size(), maxRows, pageOffset ); + rw::emitTo( out, "", + res.buildSites.size(), + secondaryCutAttrs( "build", buildPage.end - buildPage.begin, res.buildSites.size() ).c_str() ); writeCappedRows( out, "build", res.buildSites, maxRows, [ & ]( const darkflags::Site& s ) { rw::emitTo( out, "", ex( s.path ).c_str(), s.line ); - } ); + }, pageOffset ); rw::emitRaw( out, "" ); } diff --git a/src/mcp.h b/src/mcp.h index 218d60c6d..a67b220c6 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -720,7 +720,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // while the CLI --recall began honoring --top-k in this round's Wave 1. "{\"name\":\"memory_recall\",\"description\":\"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = the body ceiling in tokens (default 8000) — it SHAPES to fit, the CLI --recall's --max-tokens, not --token-budget's refuse-if-over GATE, and the header discloses max_tokens= and every cut.\"," + mcprefuse::toolMetadataFor( "memory_recall", pathIsRequired ) + "}," - "{\"name\":\"situational_awareness\",\"description\":\"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEAD'. files is a STRING of comma-separated paths (files=\\\"src/a.cpp,src/b.h\\\"), not an array; an array is refused rather than read as absent, which would answer about the working tree instead of the files you named.\"," + "{\"name\":\"situational_awareness\",\"description\":\"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEAD'. files is a STRING of comma-separated paths (files=\\\"src/a.cpp,src/b.h\\\"), not an array; an array is refused rather than read as absent, which would answer about the working tree instead of the files you named. limit/offset page blast_radius and forgotten only; with no limit every row is served, as always.\"," + mcprefuse::toolMetadataFor( "situational_awareness", pathIsRequired ) + "}," "{\"name\":\"mentions\",\"description\":\"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. " + std::string( kAtSeedRebindClause ) + "\"," + mcprefuse::toolMetadataFor( "mentions", pathIsRequired ) + "}," @@ -776,7 +776,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo + mcprefuse::toolMetadataFor( "whereis", pathIsRequired ) + "}," ) + mcprefuse::gitOnlyStanza( omitGitVerbs, "{\"name\":\"stray_content\",\"description\":\"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the work — the case `git cherry` structurally cannot see; merged branches are omitted and counted; v=unknown is a branch this scan could NOT analyse at all (no merge-base, unrelated history), not a fourth kind of divergence. Every file row carries its raw del/redone/sim evidence. Line-granular, not semantic. kind = optional ref-name substring filter, echoed as filter=; limit/offset page the refs. Single-root; read-only.\"," + mcprefuse::toolMetadataFor( "stray_content", pathIsRequired ) + "}," ) + - "{\"name\":\"flags\",\"description\":\"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is both a header gate and a CMake option the CMake default wins and the header shows as an also row. Lexical, not preprocessed: it reports the in-repo default, never the value your build used. kind = optional gate-name substring filter, echoed as filter=. symbol = optional GATE NAME, switching to the FLIP lens for that one gate: what becomes live, who holds it, what it reaches, which tests cover it. An unknown gate name is refused with near-misses, never answered empty.\"," + "{\"name\":\"flags\",\"description\":\"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is both a header gate and a CMake option the CMake default wins and the header shows as an also row. Lexical, not preprocessed: it reports the in-repo default, never the value your build used. kind = optional gate-name substring filter, echoed as filter=. symbol = optional GATE NAME, switching to the FLIP lens for that one gate: what becomes live, who holds it, what it reaches, which tests cover it. An unknown gate name is refused with near-misses, never answered empty. limit/offset page the read sites under a gate (first 8), and the flip lens's context rows (first 25); never the gate rows, which are the answer.\"," + mcprefuse::toolMetadataFor( "flags", pathIsRequired ) + "}," "{\"name\":\"doc_drift\",\"description\":\"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), `= N` constants and `[N]` array extents. Read this BEFORE trusting a design doc, plan or audit you did not just write. Every lane deliberately under-reports; checked + unchecked = anchors, each declined check named. A failed anchor the AUTHOR DATED is kind=dated-record, counted in dated= rather than drift=, so drift= is the LIVE rot. Prose, Status lines and dates are not checked. kind = optional doc-path filter, echoed as filter=; limit/offset page the docs.\"," + mcprefuse::toolMetadataFor( "doc_drift", pathIsRequired ) + "}," @@ -1443,10 +1443,13 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // built — the pre-fix arm answered it with all-empty arrays and a green _fresh, which a // caller checking only for an `error` key reads as "your edit has no blast radius". const std::string listRefusal = situationFileListRefusal( path, src ); - const std::string j = listRefusal.empty() ? situationDiffJson( path, src ) : std::string(); - resp = !listRefusal.empty() ? errResultMsg( -32602, listRefusal ) - : j.empty() ? errResult( -32602, "no changed files given and no git diff" ) - : textResult( j ); + resp = pagedResult( [ & ]( McpPageArgs pg ) // C1 F-10: blast_radius + forgotten window + { + const std::string j = listRefusal.empty() ? situationDiffJson( path, src, pg ) : std::string(); + return !listRefusal.empty() ? errResultMsg( -32602, listRefusal ) + : j.empty() ? errResult( -32602, "no changed files given and no git diff" ) + : textResult( j ); + } ); } else if( name == "mentions" && !path.empty() && !symbol.empty() ) { @@ -1516,8 +1519,14 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo if( !symbol.empty() ) { std::vector nearMisses; - const std::string t = flipText( path, symbol, flipimpact::kMaxFlipRows, nearMisses ); - if( t.empty() ) + const McpPageParse flipPage = mcpPageArgs( args ); // C1 F-07: the flip listings window + const std::string t = flipPage.refusal.empty() + ? flipText( path, symbol, flipimpact::kMaxFlipRows, nearMisses, flipPage.page ) : std::string(); + if( !flipPage.refusal.empty() ) + { + resp = errResultMsg( -32602, flipPage.refusal ); + } + else if( t.empty() ) { std::string msg = "no gate named '" + symbol + "' — call flags without `symbol` for the gate table"; if( !nearMisses.empty() ) @@ -1535,8 +1544,11 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo } else { - const std::string t = flagsText( path, kind, darkflags::kMaxSitesShown ); - resp = t.empty() ? errResult( -32603, "internal error" ) : textResult( t ); + resp = pagedResult( [ & ]( McpPageArgs pg ) // C1 F-07: the per-gate listing windows + { + const std::string t = flagsText( path, kind, darkflags::kMaxSitesShown, pg ); + return t.empty() ? errResult( -32603, "internal error" ) : textResult( t ); + } ); } } else if( name == "doc_drift" && !path.empty() ) diff --git a/src/mcprefusal.h b/src/mcprefusal.h index d1ba9c55f..2254f9681 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -980,7 +980,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { // --cochange is in that set, so its twin declares the same window. { "cochange", "path file limit offset" }, { "memory_recall", "path task top_k budget_tokens" }, - { "situational_awareness", "path diff files" }, + { "situational_awareness", "path diff files limit offset" }, { "mentions", "path paths symbol limit offset" }, { "for", "path paths task budget_tokens" }, { "lego", "path paths type legend" }, @@ -1006,7 +1006,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "edit_check", "path paths symbol new_body limit offset legend" }, { "whereis", "path symbol kind limit offset legend" }, { "stray_content", "path kind limit offset legend" }, - { "flags", "path kind symbol legend" }, + { "flags", "path kind symbol limit offset legend" }, { "doc_drift", "path kind limit offset legend" }, // lane/tc-sliceat: the ARISE def-use slice — var/flow/depth mirror the CLI's :VAR / --slice-flow / // --slice-depth knobs; single-root by kMcpSingleRootVerbs (a per-definition on-disk re-parse). diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 59517cebc..9395345ae 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -311,6 +311,15 @@ inline constexpr long long kMcpPageValueMax = 1000000000; // == cli.h's kPageV // value outside the band is refused rather than quietly rewritten (the §B8.1 ruling, same as radius). inline constexpr long long kMcpRecallTopKMax = 1000; +// C1 F-07: the verb-side fold of pageview.h's effectiveRowCap — "an explicit limit beats the verb's own +// display default" in ONE place on this surface too. It exists because writing that line twice, once in +// flagsText and once in flipText, is what --quality-delta reads as a new clone of a reused helper, and it +// is right to: two copies of a cap decision is one more than the contract needs. +inline std::size_t mcpRowCap( int pageLimit, std::size_t verbDefault ) noexcept +{ + return std::size_t( rw::effectiveRowCap( pageLimit, int( verbDefault ) ) ); +} + inline McpPageParse mcpPageArgs( const std::string& scope ) { const McpIntArg limitArg = mcpIntArg( scope, "limit", 1, kMcpPageValueMax ); @@ -513,11 +522,15 @@ inline std::string strayContentText( const std::string& root, const std::string& } // `flags` verb: the dark-content dashboard. Index-backed (it needs the crawled file list). -inline std::string flagsText( const std::string& root, const std::string& filter, std::size_t maxSites ) +// C1 F-07 (2026-09-10): --flags joined cli.h's honorsPaging set when its per-gate listing became +// windowable, so the twin takes the same pair rather than 0,0 — M13's rule is that a CLI verb that pages has +// a twin that pages, and test/mcpcontractcheck.sh (G) derives that set from kPagingHonoringVerbs itself. +inline std::string flagsText( const std::string& root, const std::string& filter, std::size_t maxSites, + McpPageArgs page = {} ) { const McpIndex& ix = getIndex( root ); const darkflags::FlagsResult res = darkflags::computeFlags( ix.ing, root, {}, filter ); - return captureXml( [ & ]( std::FILE* f ) { darkflags::writeFlags( f, res, maxSites ); } ); + return captureXml( [ & ]( std::FILE* f ) { darkflags::writeFlags( f, res, mcpRowCap( page.limit, maxSites ), page.offset ); } ); } // `flags` verb with the optional `symbol` argument = the CLI's `--flags --flip=NAME`: the blast radius of @@ -526,12 +539,12 @@ inline std::string flagsText( const std::string& root, const std::string& filter // index-backed, and unlike the plain lane it needs the call graph too (ix.g). "" ⇒ no such gate: the // handler turns that into a -32602 naming the near-misses, never an empty-looking success. inline std::string flipText( const std::string& root, const std::string& gate, std::size_t maxRows, - std::vector& nearMissesOut ) + std::vector& nearMissesOut, McpPageArgs page = {} ) { const McpIndex& ix = getIndex( root ); - const flipimpact::FlipResult res = flipimpact::computeFlip( ix.ing, ix.g, root, {}, gate ); + const flipimpact::FlipResult res = flipimpact::computeFlip( ix.ing, ix.g, root, {}, gate, page.limit ); if( !res.ok ) { nearMissesOut = res.nearMisses; return {}; } - return captureXml( [ & ]( std::FILE* f ) { flipimpact::writeFlip( f, res, ix.ing, root, maxRows ); } ); + return captureXml( [ & ]( std::FILE* f ) { flipimpact::writeFlip( f, res, ix.ing, root, mcpRowCap( page.limit, maxRows ), page.offset ); } ); } // `doc_drift` verb: the markdown docs' checkable anchors vs the live index. Index-backed — @@ -1171,7 +1184,14 @@ inline std::string declDefAndWindowJson( const SituationFacts& facts, PathRelFn + std::to_string( facts.coCommits ); } -inline std::string situationDiffJson( const std::string& root, const std::string& diffOrEmpty ) +// C1 F-10 (2026-09-10): --situ joined cli.h's honorsPaging set (its blast-radius and co-change sections +// window), so this twin takes limit/offset too — M13's rule, derived by test/mcpcontractcheck.sh (G) from +// kPagingHonoringVerbs. THE DEFAULT IS DIFFERENT ON PURPOSE, and it is the honest one: the CLI report caps +// those two listings at 8 because it is a screen an agent reads inline, while this payload is machine-read +// and has always served EVERY row. An absent limit therefore still serves every row — this adds relief for +// a caller who wants less, never a new cut — and the two arrays are the only ones windowed: tests_to_run and +// hotspot_alert are the answer, exactly as in the CLI twin. +inline std::string situationDiffJson( const std::string& root, const std::string& diffOrEmpty, McpPageArgs page = {} ) { const McpIndex& ix = getIndex( root ); const IngestResult& ing = ix.ing; @@ -1245,10 +1265,12 @@ inline std::string situationDiffJson( const std::string& root, const std::string // payload used to emit {"file":...} alone, so the agent got a ranked blast radius with no magnitude and // could not tell a file contributing 300 dependent symbols from one contributing 1 — while the CLI text // report has printed "(N dependent symbols)" on every such line all along. + const PageWindow situJBlast = pageWindow( facts.blastRadius.size(), page.limit, page.offset ); + const PageWindow situJForgot = pageWindow( facts.forgotten.size(), page.limit, page.offset ); out += "],\"blast_radius\":["; { bool first = true; - for( std::size_t i = 0; i < facts.blastRadius.size(); ++i ) + for( std::size_t i = situJBlast.begin; i < situJBlast.end; ++i ) { if( !first ) { @@ -1288,8 +1310,9 @@ inline std::string situationDiffJson( const std::string& root, const std::string out += "]" + declDefAndWindowJson( facts, situJPathRel ) + ",\"forgotten\":["; { bool first = true; - for( const auto& [ f, deg ] : facts.forgotten ) + for( std::size_t i = situJForgot.begin; i < situJForgot.end; ++i ) { + const auto& [ f, deg ] = facts.forgotten[i]; if( !first ) { out += ","; diff --git a/src/pageview.h b/src/pageview.h index af706c3d8..059b1d485 100644 --- a/src/pageview.h +++ b/src/pageview.h @@ -320,6 +320,43 @@ inline const char* pageDisclosure( char* buf, std::size_t bufCap, std::size_t ro return buf; } +// ── LB-G (listing-paging round, 2026-09-10) — the SECONDARY listing's pair, emitted ONLY on a CUT ──────── +// +// Rule 6 reserves the paging half for a report's PRIMARY listing; a SECONDARY one "discloses through its own +// shown_=/_capped= pair". Three reports had that listing and no pair at all: --doc-drift's per-doc +// rows (cut at 12), --flags' per-gate rows (cut at 8) and --flip's six row families (cut at 25). +// They are per-CHILD listings — one per , one per — so there is no single root window to page, +// and the honest disclosure is the pair on the child that was cut. +// +// EMITTED ONLY WHEN THE CUT HAPPENED, both halves together or neither. Rule 3 says capped= is always emitted +// beside its shown=, and that is exactly what this does — what it does NOT do is emit shown_="8" +// _capped="0" on the ninety-nine children that fit, which on --flags would be 86 of 88 gates paying +// bytes to say nothing was dropped. Rule 3's own sentence sanctions the shape ("If a verb emits no shown=, +// it emits no capped= either — --skill-scan emits the pair only on a capped scan; that is conformant"), and +// the round's rule 1 requires it: a capped="0" is a disclosure that never fires, and the reader cannot tell +// it from one that cannot fire. +// +// `totalAttr` is rule 2's total: pass nullptr when the element ALREADY carries the row total under its own +// name (--flags' reads=, --doc-drift's ), and a name when it does not (--doc-drift's , +// whose row population is drift= + dated= and has no single attribute — a reader should not have to sum two +// numbers to learn what was cut). A caller must never pass a name the element already uses for something +// else: `anchors=` on counts EVERY anchor in the doc, not the failed ones these rows list, and reusing +// it would rebuild the dark=/dark_gates= count-vs-bool collision §P8 renamed its way out of. +inline std::string secondaryCutAttrs( const char* noun, std::size_t shown, std::size_t total, const char* totalAttr = nullptr ) +{ + if( shown >= total ) + { + return {}; // nothing was cut: the element is byte-identical to what it was + } + std::string a = " shown_" + std::string( noun ) + "=\"" + std::to_string( shown ) + + "\" " + std::string( noun ) + "_capped=\"1\""; + if( totalAttr != nullptr ) + { + a += " " + std::string( totalAttr ) + "=\"" + std::to_string( total ) + "\""; + } + return a; +} + // The PAGING HALF ALONE — rule 1's noun-prefixed exception, and the ONLY sanctioned way to emit a page // without a bare shown=. A report with several INDEPENDENT listings already spells its primary listing's // row count as shown_=/_capped= (--communities' shown_modules=/modules_capped=, which stay diff --git a/src/situ.h b/src/situ.h index a2f14148e..1a0bcf008 100644 --- a/src/situ.h +++ b/src/situ.h @@ -345,18 +345,58 @@ inline std::vector declDefPartners( const IngestResult& ing, con return out; } -inline constexpr std::size_t kSituBlastFilesShown = 8; // section [1] — blast-radius file rows -inline constexpr std::size_t kSituTestRowsShown = 25; // section [2] — tests-to-run rows -inline constexpr std::size_t kSituPartnerRowsShown = 8; // section [3] — co-change partner rows +// C1 F-10 (2026-09-10): --situ is the mid-task verb CLAUDE.md's protocol names, and it cut section [1] to +// 8 of 69 files and section [3] to 8 of 116 partners with the cut stated in PROSE ONLY and --limit REFUSED — +// so the one report an agent runs mid-change had no relief on its two widest listings. Both are CONTEXT and +// now page through pageview.h like every other listing in the tool (--limit=N raises the default, --offset=M +// pages it). Section [2], tests to run, is the ANSWER — you act on those rows, --test-gate exits 4 on them, +// and its sibling listing has never been windowed — so it is served WHOLE and has no cap at all any more +// (kSituTestRowsShown is deleted rather than raised: a cap on an answer is the finding, not the number). +inline constexpr std::size_t kSituBlastFilesShown = 8; // section [1] — blast-radius file rows; a raisable DEFAULT +inline constexpr std::size_t kSituPartnerRowsShown = 8; // section [3] — co-change partner rows; a raisable DEFAULT inline constexpr std::size_t kSituPartnerFileRowsShown = 4; // section [1] — decl/def partner rows -inline std::string situShowingNote( std::size_t shownCap, std::size_t rowTotal, const char* rowNoun ) +// §B12.1 gave this the "showing N of M " form so a reader could see the gap without a second sentence; +// C1 F-10 adds the machine half — pageview.h's shown=/total=/capped= spelled in prose, because --situ has no +// XML root to carry attributes — and the exact pasteable follow-up. All of it appears ONLY on a cut section: +// an untruncated section is byte-unchanged, and no section ever prints capped=0. +inline std::string situShowingNote( std::size_t shown, std::size_t rowTotal, const char* rowNoun, + std::string_view nextInvocation = {}, std::string_view extraProse = {} ) { - if( rowTotal <= shownCap ) + if( rowTotal <= shown ) { return {}; } - return " (showing " + std::to_string( shownCap ) + " of " + std::to_string( rowTotal ) + " " + rowNoun + ")"; + // ORDER IS THE CONTRACT: prose first, then the machine triple, then `next:` LAST — a pasteable command has + // to run to the end of the parenthetical or a reader cannot tell where it stops. `extraProse` is the one + // section-specific sentence (section [1] pointing at --pr-context's own cap) that used to be spliced in by + // hand at size() - 1, which put it AFTER the command. + std::string note = " (showing " + std::to_string( shown ) + " of " + std::to_string( rowTotal ) + " " + rowNoun; + note += std::string( extraProse ); + note += " — shown=" + std::to_string( shown ) + " total=" + std::to_string( rowTotal ) + " capped=1"; + if( !nextInvocation.empty() ) + { + note += "; next: " + std::string( nextInvocation ); + } + return note + ")"; +} + +// The three facts --situ's two context sections need about the caller's window, as ONE parameter rather +// than three: writeSituation already sat at 6 parameters, over the bar, and three more would have been the +// largest single params regression in the round that is about not letting a listing grow silently. +struct SituPageArgs +{ + int limit = 0; // 0 = the verb's own default row caps (8 and 8) + int offset = 0; + std::string_view selector; // the caller's own --situ spelling, echoed back in `next:` +}; + +// The exact `--situ … --limit=N` that cuts nothing, built from the selector the caller was actually given so +// it pastes back verbatim (bare --situ reads the git diff; --situ=F1,F2 named its own files). +inline std::string situNextInvocation( std::string_view selector, std::size_t needed ) +{ + const std::string verb = selector.empty() ? std::string( "--situ" ) : ( "--situ=" + std::string( selector ) ); + return verb + " --limit=" + std::to_string( needed ); } // Section [1]'s decl/def rows and section [3]'s empty-co-change line, as their own emitters: writeSituation @@ -393,7 +433,8 @@ inline void writeSituEmptyCochangeLine( std::FILE* out, std::size_t coCommits ) inline void writeSituation( std::FILE* out, const std::string& root, const IngestResult& ing, const Graph& g, const std::vector& changedFile, - std::uint32_t onlyRoot = UINT32_MAX ) // multi-root §5: co-change mined within that root only + std::uint32_t onlyRoot = UINT32_MAX, // multi-root §5: co-change mined within that root only + SituPageArgs page = {} ) // C1 F-10: sections [1] and [3] page; section [2] is the answer and never does { const std::uint32_t F = std::uint32_t( ing.files.size() ); const std::uint32_t N = std::uint32_t( ing.symbols.size() ); @@ -489,11 +530,12 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // §B12.1: "(showing 8" carried neither a UNIT nor a remainder, so a reader who noticed the 8 rows summed // to 59 of the stated 69 symbols had no way to tell whether 8 counted files, symbols or something else. // "showing 8 of 17 files" self-explains the gap without a second sentence. - std::string blastNote = situShowingNote( kSituBlastFilesShown, affected.size(), "files" ); - if( !blastNote.empty() ) - { - blastNote.insert( blastNote.size() - 1, "; --pr-context's own per-file blast-radius list is also capped, at 20" ); - } + // C1 F-10: the window is pageview.h's, so --limit=N raises the 8 and --offset=M pages it. + const PageWindow blastPage = pageWindow( affected.size(), effectiveRowCap( page.limit, int( kSituBlastFilesShown ) ), page.offset ); + const std::size_t blastShown = blastPage.end - blastPage.begin; + const std::string blastNote = situShowingNote( blastShown, affected.size(), "files", + situNextInvocation( page.selector, affected.size() ), + "; --pr-context's own per-file blast-radius list is also capped, at 20" ); rw::emitTo( out, " [1] blast radius: {} symbols across {} files transitively depend on these changes{}\n", reach.size(), affected.size(), blastNote.c_str() ); // F3: the decl/def partner FIRST — it is the answer to "what else has to change with this file" that the @@ -505,7 +547,7 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges for( std::uint32_t k : g.unresolvedOut ) { gaugeUnresolved += k; } rw::emitTo( out, kGraphCountFloorTextLine, gaugeAmb, gaugeUnresolved, graphUnindexedTextClause( g.unindexedFiles ).c_str() ); } - for( std::size_t i = 0; i < affected.size() && i < kSituBlastFilesShown; ++i ) + for( std::size_t i = blastPage.begin; i < blastPage.end; ++i ) { const std::string_view rp = situPathRel( affected[i] ); rw::emitTo( out, " {} ({} dependent symbols)\n", std::string_view( rp.data(), rp.size() ), fileReachers[ affected[i] ] ); @@ -524,13 +566,16 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges } // §H6 (W3FIX): this header printed the FULL count then listed at most 25 rows, silently — on the one // section whose sibling --test-gate calls its rows "the COMPLETE obligation". - rw::emitTo( out, " [2] tests to run ({}){}{}", tests.size(), situShowingNote( kSituTestRowsShown, tests.size(), "tests" ).c_str(), + // C1 F-10: this listing had a 25-row cap and no relief. It is the ANSWER — the rows you run, the rows + // --test-gate exits 4 on — so it is served whole and carries no showing-note at all: there is nothing to + // disclose when nothing can be dropped. + rw::emitTo( out, " [2] tests to run ({}){}", tests.size(), tests.empty() ? ": (none transitively reach these files)\n" : " — evidence order: [changed] you edited it, [partner] named after a changed file, then hops (1 = calls a changed symbol directly):\n" ); // §P11.4: this section says "tests to run" and named files that are not commands. The runner is appended // where one is DERIVABLE and omitted where it is not — see testmap.h; a guessed command is worse than none. const TestRunnerIndex situRunners( ing ); - for( std::size_t i = 0; i < testRows.size() && i < kSituTestRowsShown; ++i ) + for( std::size_t i = 0; i < testRows.size(); ++i ) { const TestRow& r = testRows[i]; const std::string_view rp = situPathRel( r.fileId ); @@ -581,14 +626,17 @@ inline void writeSituation( std::FILE* out, const std::string& root, const Inges // §H6 (W3FIX): same undisclosed cap as [2] — 18 partners, 8 rows on this repo's own src/graph.h probe. // F2: window=/commits= ride on the header, so the count below is readable as a measurement — or as the // absence of one. --cochange, the component this composes, already emits both. + const PageWindow partnerPage = pageWindow( partners.size(), effectiveRowCap( page.limit, int( kSituPartnerRowsShown ) ), page.offset ); + const std::size_t partnerShown = partnerPage.end - partnerPage.begin; rw::emitTo( out, " [3] co-change — usually edited with these but NOT in your diff ({}) window=\"{}\" commits=\"{}\"{}:\n", partners.size(), coWindow.c_str(), coCommits, - situShowingNote( kSituPartnerRowsShown, partners.size(), "files" ).c_str() ); + situShowingNote( partnerShown, partners.size(), "files", + situNextInvocation( page.selector, partners.size() ) ).c_str() ); if( partners.empty() ) { writeSituEmptyCochangeLine( out, coCommits ); } - for( std::size_t i = 0; i < partners.size() && i < kSituPartnerRowsShown; ++i ) + for( std::size_t i = partnerPage.begin; i < partnerPage.end; ++i ) { const std::string_view rp = situPathRel( partners[i].first ); rw::emitTo( out, " {} (co-edited in {:.0f}% of commits)\n", std::string_view( rp.data(), rp.size() ), partners[i].second * 100.0 ); diff --git a/src/verbs_change.h b/src/verbs_change.h index b9907955c..184d70754 100644 --- a/src/verbs_change.h +++ b/src/verbs_change.h @@ -367,7 +367,8 @@ std::optional runChangeViews( const MainDispatch& d ) "a selector fact, not an empty diff)\n", std::string_view( cfg.situFiles.data(), cfg.situFiles.size() ), elsewhere.c_str() ); continue; } - rw::writeSituation( stdout, ws[r].arg, ing, g, perRootChanged[r], r ); + rw::writeSituation( stdout, ws[r].arg, ing, g, perRootChanged[r], r, + rw::SituPageArgs{ cfg.pageLimit, cfg.pageOffset, cfg.situFiles } ); } return 0; } @@ -391,7 +392,8 @@ std::optional runChangeViews( const MainDispatch& d ) if( !gitChangedFiles( root, ing, changed ) ) { rw::emitRaw( stderr, "ripwire --situ: no files given and no git diff (use --situ=F1,F2)\n" ); return 1; } } - rw::writeSituation( stdout, root, ing, g, changed ); + rw::writeSituation( stdout, root, ing, g, changed, UINT32_MAX, + rw::SituPageArgs{ cfg.pageLimit, cfg.pageOffset, cfg.situFiles } ); return 0; } @@ -1384,7 +1386,7 @@ int runFlip( const MainDispatch& d ) return 1; } - const flipimpact::FlipResult result = flipimpact::computeFlip( d.ing, d.g, root, d.cfg.excludes, d.cfg.flipGate ); + const flipimpact::FlipResult result = flipimpact::computeFlip( d.ing, d.g, root, d.cfg.excludes, d.cfg.flipGate, d.cfg.pageLimit ); if( !result.ok ) { std::string msg = "ripwire: --flip: no gate named '" + std::string( d.cfg.flipGate ) + "' in " + root; @@ -1396,12 +1398,21 @@ int runFlip( const MainDispatch& d ) msg += ( i ? ", '" : " '" ) + result.nearMisses[i] + "'"; } msg += "?)"; + // C1 F-07: the suggestion list is capped, and a cap nobody is told about on the one output a + // lost caller reads is the same silent cut this round closed in the report itself. + if( result.nearMissTotal > result.nearMisses.size() ) + { + msg += " (showing " + std::to_string( result.nearMisses.size() ) + " of " + + std::to_string( result.nearMissTotal ) + " candidates — --limit=N raises it)"; + } } rw::emitTo( stderr, "{}\n", msg.c_str() ); rw::emitTo( stderr, "ripwire: run `ripwire {} --flags` for the gate table\n", root.c_str() ); return 1; } - flipimpact::writeFlip( stdout, result, d.ing, root, d.cfg.detail ? SIZE_MAX : flipimpact::kMaxFlipRows ); + flipimpact::writeFlip( stdout, result, d.ing, root, + d.cfg.detail ? SIZE_MAX : std::size_t( rw::effectiveRowCap( d.cfg.pageLimit, int( flipimpact::kMaxFlipRows ) ) ), + d.cfg.pageOffset ); return 0; } @@ -1584,7 +1595,11 @@ std::optional runCrossRef( const MainDispatch& d ) "list them, e.g. --flags=RIPWIRE)\n", std::string_view( cfg.darkFlagsFilter.data(), cfg.darkFlagsFilter.size() ) ); return 1; } - darkflags::writeFlags( stdout, result, cfg.detail ? SIZE_MAX : darkflags::kMaxSitesShown ); + // C1 F-07: the per-gate cap is a raisable DEFAULT now — --limit=N beats it through the + // tool-wide effectiveRowCap rule, --detail still lifts it outright, and --offset=M pages it. + darkflags::writeFlags( stdout, result, + cfg.detail ? SIZE_MAX : std::size_t( rw::effectiveRowCap( cfg.pageLimit, int( darkflags::kMaxSitesShown ) ) ), + cfg.pageOffset ); return 0; } diff --git a/test/docdriftfix.golden.xml b/test/docdriftfix.golden.xml index b4130a373..ec1867cd3 100644 --- a/test/docdriftfix.golden.xml +++ b/test/docdriftfix.golden.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/listingpagingcheck.sh b/test/listingpagingcheck.sh new file mode 100755 index 000000000..a4b674174 --- /dev/null +++ b/test/listingpagingcheck.sh @@ -0,0 +1,502 @@ +#!/usr/bin/env bash +# listingpagingcheck.sh — three LISTING verbs learn to page honestly: --doc-drift, --flags (with --flip) +# and --situ. The class is the one PR #108 closed on --edit-check, found again by the 2026-09-10 cap audit +# (C1 rows F-06, F-07, F-10) on the three verbs whose row listings no flag could reach: +# +# F-06 --doc-drift cut its per-doc listing at kMaxAnchorsShown=12 and disclosed it with nothing but a +# remainder, while no output named the flag that lifts it. The audit's suggested fix — +# route the cap through effectiveRowCap so --limit raises it — was BUILT AND REJECTED, with the +# evidence coming from test/pagingsweepcheck.sh: the rows are a SECONDARY listing under the +# PRIMARY rows that --limit already windows, so one flag governing both makes the same doc +# print shown_failed="3" at --limit=3 and shown_failed="6" at --limit=6 — page[0:3] + page[3:6] +# stops equalling page[0:6] and the --offset continuity arm goes red, correctly. The tool had +# already settled this shape (pageview.h kImportReachRowCap: a secondary listing "is NOT raisable +# by --limit … discloses through shown_importers=/importers_capped= and nothing else"), so what +# closes here is the SILENCE: the pair on the doc that was cut, and next= naming --detail. +# F-07 src/darkflags.h and src/flipimpact.h emitted NO shown=/total=/capped= token at all (grep: 0 hits +# in either file). --flags cut the sites under a gate at 8 and --flip cut six listings at 25, +# both in silence but for a bare remainder, and no flag lifted either. +# F-10 --situ — the mid-task verb CLAUDE.md's own protocol names — cut its blast radius to 8 of 69 files +# and its co-change partners to 8 of 116, said so in PROSE ONLY, and REFUSED --limit outright. +# +# WHICH ROWS ARE THE ANSWER, AND THEREFORE NEVER PAGE. This is the whole design, so it is stated here and +# asserted below rather than left to the reader of a diff. It is read out of each verb's OWN legend: +# +# --flags the rows ARE the answer ("what is BUILT but DARK here"). They are never windowed, +# capped or paged. What pages is the read SITES under one gate — context for a gate row. +# --flip "tests = test files reaching the hosts" is the tests_to_run family (flipimpact.h M21(b)), +# the rows you RUN. They are served whole on every page, exactly as --test-gate's listing +# always has been. Everything else — lights r/b, hosts, downstream, untested, build sites — +# is context and pages at 25 by default. (untested= pages like --test-gate's rows, whose +# verdict is the root COUNT and the exit code, not the emitted rows.) +# --situ section [2], tests to run, is the answer: --test-gate exits 4 on exactly those rows. It has +# no cap at all any more. Sections [1] blast radius and [3] co-change partners are context. +# --doc-drift there is no answer LISTING — the answer is the per-doc verdict (drift=/dated=), which is +# computed over the full anchor set. Every row is context; it is capped, disclosed, and +# lifted by --detail rather than windowed by --limit (see F-06 above). +# +# ARMS. Each verb gets the capdisclosurecheck triple plus the decisive one: +# (CROSSING) the fixture really is past the cap, proved from the UNCAPPED run's own row count — a green +# arm can never be a fixture that never reached the code under test. +# (DISCLOSURE) the cut answer says so in pageview.h's vocabulary (shown_=/_capped=, or the +# prose triple for --situ) AND carries next=, the exact pasteable follow-up. +# (RE-DERIVATION) THE DECISIVE ONE. The same binary is run twice — default, and at the --limit the +# answer's own next= names — and every VERDICT/count attribute must be BYTE-IDENTICAL. +# "Does the bound trip" is green for a broken emitter too; "does the answer move when the +# bound is removed" is not. +# (SILENCE) a fixture that FITS carries none of the pair, and NOTHING anywhere emits a *_capped="0" +# of the new family or a shown_= equal to its total (the round's rule 1: a disclosure +# that can never fire is indistinguishable from one that cannot). +# (ANSWER) the answer rows above are complete at the default cap — count them. +# (MUTATION) the RE-DERIVATION comparison can go red. Done on a SYNTHESIZED document, not a scratch +# build: test/pargates.py runs this gate under a wall budget, and a compile inside a budgeted +# gate is the failure mode recorded as "a build inside a budgeted gate" — super-linear under +# contention, and a bigger budget cannot fix it. The mutation rewrites a verdict attribute to +# the WINDOW's row count, which is precisely the bug the arm exists to catch, and the arm +# must report it. +# +# MUTATION CONTROL (the red run this gate was written from): against a binary built before this change — +# RIPWIRE_BIN=/ripwire bash test/listingpagingcheck.sh +# — 24 of 33 checks FAIL, measured at 6afaa457 on 2026-09-10. Note that the CROSSING halves fail there too, +# and that is not a weakness of the fixture: crossing is proved from the run at --limit=1000000, and the +# whole finding is that the pre-change binary does not honour it (--doc-drift served the same 12 rows, +# --flags/--flip/--situ REFUSED the flag). The three that still pass are (E)'s two silence arms and the +# per-doc verdict comparison — which is the point: those were already correct, and this change keeps them so. +# +# Usage: +# bash test/listingpagingcheck.sh +# RIPWIRE_BIN=asan/ripwire bash test/listingpagingcheck.sh +# +# Exits non-zero on any failure; prints PASS/FAIL per check and ALL PASS on success. Needs git + python3. + +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' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +command -v git >/dev/null 2>&1 || { echo "listingpagingcheck: git required"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "listingpagingcheck: python3 required"; exit 2; } + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +echo "listingpagingcheck: BIN=$BIN" + +# Every cap is read from the source that DEFINES it, never retyped: a gate holding its own copy of "8" goes +# green the day the constant moves and the emitter stops agreeing with it. +capof(){ sed -n "s/.*$2 *= *\([0-9][0-9]*\).*/\1/p" "$ROOT/src/$1" | head -1; } +CAP_ANCHORS="$( capof docdrift.h kMaxAnchorsShown )" +CAP_SITES="$( capof darkflags.h kMaxSitesShown )" +CAP_FLIP="$( capof flipimpact.h kMaxFlipRows )" +CAP_BLAST="$( capof situ.h kSituBlastFilesShown )" +CAP_PARTNER="$( capof situ.h kSituPartnerRowsShown )" +for v in CAP_ANCHORS CAP_SITES CAP_FLIP CAP_BLAST CAP_PARTNER; do + eval "n=\$$v" + case "$n" in ''|*[!0-9]*) echo "listingpagingcheck: could not read $v from src/"; exit 2 ;; esac +done +echo " (kMaxAnchorsShown=$CAP_ANCHORS kMaxSitesShown=$CAP_SITES kMaxFlipRows=$CAP_FLIP kSituBlastFilesShown=$CAP_BLAST kSituPartnerRowsShown=$CAP_PARTNER)" + +# --------------------------------------------------------------------------------------------------- +# FIXTURES, sized FROM the caps above so none of them lands one row short of the bound it is testing. +# +# DD one markdown doc carrying CAP_ANCHORS*3 failed anchors — backticked names this tree defines +# NOWHERE, each on a line that also names a real symbol (docdrift.h kCorroborateWin: a mention only +# counts as a claim about this code when a name the repo DOES define sits on the same line). +# FL one header gate read from CAP_FLIP*2 files, each read inside its own `#if` region — so the SAME +# fixture crosses --flags' per-gate site cap AND --flip's per-listing row cap. +# ST a git tree where one header is called by CAP_BLAST*6 files (blast radius) and CAP_PARTNER*3 more +# files were committed alongside it in every commit of its history (co-change partners), so both of +# --situ's context sections are past their caps while section [2] stays the answer. +# FIT the SILENCE control: one tiny doc, one gate with one read, one file. Nothing can be cut. +# --------------------------------------------------------------------------------------------------- +python3 - "$TMP" "$CAP_ANCHORS" "$CAP_SITES" "$CAP_FLIP" "$CAP_BLAST" "$CAP_PARTNER" <<'PY' +import os, sys +tmp = sys.argv[1] +anchors, sites, fliprows, blast, partner = ( int( a ) for a in sys.argv[2:7] ) + +def w( path, text ): + os.makedirs( os.path.dirname( path ), exist_ok=True ) + open( path, "w" ).write( text ) + +# ── DD ─────────────────────────────────────────────────────────────────────────────────────────────── +nAnchors = anchors * 3 +w( tmp + "/dd/src/code.h", + "#pragma once\n" + "".join( "inline int realAnchorSymbol%03d() { return %d; }\n" % ( i, i ) for i in range( nAnchors ) ) ) +lines = [ "# Drift fixture\n\n" ] +for i in range( nAnchors ): + lines.append( "- `realAnchorSymbol%03d` is wired to `zzqPhantomGadget%03d` in the pipeline.\n" % ( i, i ) ) +w( tmp + "/dd/DOC.md", "".join( lines ) ) + +# ── FL ─────────────────────────────────────────────────────────────────────────────────────────────── +nSites = fliprows * 2 +w( tmp + "/fl/wide.h", "#pragma once\n#ifndef FIXTURE_WIDE_GATE\n#define FIXTURE_WIDE_GATE 0\n#endif\n" ) +for i in range( nSites ): + w( tmp + "/fl/u%03d.cpp" % i, + '#include "wide.h"\n' + "int wideUser%03d()\n{\n#if FIXTURE_WIDE_GATE\n return %d;\n#endif\n return 0;\n}\n" % ( i, i ) ) +# …and one test file per host, well past kMaxFlipRows, so "the tests_to_run rows are never cut" is an +# assertion about a listing that WOULD be cut rather than about an empty one. +for i in range( nSites ): + w( tmp + "/fl/test/t%03d_test.cpp" % i, + "int wideUser%03d();\nint checkWide%03d() { return wideUser%03d(); }\n" % ( i, i, i ) ) + +# ── ST ─────────────────────────────────────────────────────────────────────────────────────────────── +nUsers = blast * 6 +nPartners = partner * 3 +w( tmp + "/st/core.h", "#pragma once\ninline int coreEntryPoint() { return 7; }\n" ) +for i in range( nUsers ): + w( tmp + "/st/u%03d.cpp" % i, '#include "core.h"\nint stUser%03d() { return coreEntryPoint(); }\n' % i ) +for i in range( nPartners ): + w( tmp + "/st/p%03d.cpp" % i, "int stPartner%03d() { return %d; }\n" % ( i, i ) ) +# section [2] is the ANSWER, so it needs more test rows than the cap it used to carry (25) for the "never +# cut" arm to mean anything. +for i in range( nUsers ): + w( tmp + "/st/test/t%03d_test.cpp" % i, '#include "../core.h"\nint stCheck%03d() { return coreEntryPoint(); }\n' % i ) + +# ── FIT ────────────────────────────────────────────────────────────────────────────────────────────── +w( tmp + "/fit/src/code.h", + "#pragma once\n#ifndef FIXTURE_TINY_GATE\n#define FIXTURE_TINY_GATE 0\n#endif\n" + "inline int tinyRealSymbol() { return 1; }\n#if FIXTURE_TINY_GATE\ninline int tinyDark() { return 2; }\n#endif\n" ) +w( tmp + "/fit/DOC.md", "# Tiny\n\n- `tinyRealSymbol` talks to `zzqPhantomOnlyOne` once.\n" ) +PY + +# ST's git history: EVERY commit touches core.h together with every p###.cpp, so the co-change miner sees a +# wide partner set; the working tree then modifies core.h alone. Committed with an isolated identity and +# `git -C` throughout (no cd, no reliance on the caller's config). +gitq(){ git -C "$TMP/st" -c user.name=gate -c user.email=gate@example.com -c commit.gpgsign=false "$@" >/dev/null 2>&1; } +gitq init -q +gitq add -A +gitq commit -q -m "base" +i=0 +while [ "$i" -lt 5 ]; do + printf '// touch %s\n' "$i" >> "$TMP/st/core.h" + j=0 + while [ "$j" -lt "$(( CAP_PARTNER * 3 ))" ]; do + printf '// touch %s\n' "$i" >> "$TMP/st/p$( printf '%03d' "$j" ).cpp" + j=$(( j + 1 )) + done + gitq add -A + gitq commit -q -m "round $i" + i=$(( i + 1 )) +done +printf 'inline int coreEntryPointTwo() { return 8; }\n' >> "$TMP/st/core.h" + +run(){ dir="$1"; out="$2"; shift 2; "$BIN" "$dir" "$@" >"$TMP/$out" 2>"$TMP/$out.err"; } + +# The one shared reader: pull an attribute off the FIRST element with the given tag. +attr(){ python3 - "$1" "$2" "$3" <<'PY' +import re, sys +doc = open( sys.argv[1], errors="replace" ).read() +body = re.sub( r'\A(?:\s*)+', '', doc, flags=re.S ) +m = re.search( r'<' + re.escape( sys.argv[2] ) + r'((?:\s+[\w:.-]+="[^"]*")*)\s*/?>', body ) +if not m: sys.exit( 0 ) +v = re.search( r'\s' + re.escape( sys.argv[3] ) + r'="([^"]*)"', m.group( 1 ) ) +print( v.group( 1 ) if v else "" ) +PY +} +countrows(){ grep -o "<$2 " "$TMP/$1" | wc -l | tr -d ' '; } + +# =================================================================================================== +echo "=== (A) --doc-drift: the per-doc listing pages, and the per-doc VERDICT does not (F-06) ===" +# =================================================================================================== +run "$TMP/dd" dd_def --doc-drift --no-cache +run "$TMP/dd" dd_all --doc-drift --no-cache --detail=1 +A_DEF="$( countrows dd_def a )" +A_ALL="$( countrows dd_all a )" + +# CROSSING — proved from the UNCAPPED run, not asserted from the fixture's source +if [ "$A_ALL" -gt "$CAP_ANCHORS" ]; then ok "(A) crossing: the uncapped run emits $A_ALL rows, past the $CAP_ANCHORS cap" +else no "(A) crossing: uncapped run emits only $A_ALL rows — the fixture never reaches the cap ($( head -c 200 "$TMP/dd_all" ))"; fi +if [ "$A_ALL" -gt "$A_DEF" ]; then ok "(A) --detail lifts the per-doc anchor cap: $A_DEF rows bare, $A_ALL under --detail" +else no "(A) --detail does not lift the anchor cap ($A_DEF bare, $A_ALL under --detail)"; fi +# THE SECONDARY-LISTING RULE (pageview.h rule 6): --limit windows the ROWS and must NOT reach into the +# listing under one of them, or a doc's own row count starts depending on which page served it — and +# page[0:3] + page[3:6] stops equalling page[0:6], which is pagingsweepcheck's --offset continuity contract. +A_L3="$( "$BIN" "$TMP/dd" --doc-drift --no-cache --limit=3 2>/dev/null | grep -o "]*>" | head -1 )" +A_L6="$( "$BIN" "$TMP/dd" --doc-drift --no-cache --limit=6 2>/dev/null | grep -o "]*>" | head -1 )" +if [ -n "$A_L3" ] && [ "$A_L3" = "$A_L6" ]; then + ok "(A) rule 6: the first element is byte-identical at --limit=3 and --limit=6 — the window does not reach the secondary listing" +else + no "(A) rule 6: the element CHANGES with --limit ($A_L3 vs $A_L6) — a paged walk is no longer equivalent to the whole" +fi + +# DISCLOSURE +DD_SHOWN="$( attr "$TMP/dd_def" doc shown_failed )" +DD_CAP="$( attr "$TMP/dd_def" doc failed_capped )" +DD_TOT="$( attr "$TMP/dd_def" doc failed_total )" +DD_NEXT="$( attr "$TMP/dd_def" doc-drift next )" +if [ "$DD_SHOWN" = "$CAP_ANCHORS" ] && [ "$DD_CAP" = "1" ] && [ -n "$DD_TOT" ] && [ "$DD_TOT" -gt "$CAP_ANCHORS" ]; then + ok "(A) disclosure: " +else + no "(A) disclosure: the cut says shown_failed=\"$DD_SHOWN\" failed_capped=\"$DD_CAP\" failed_total=\"$DD_TOT\" — the cut is not disclosed" +fi +case "$DD_NEXT" in + --doc-drift\ --detail=1) ok "(A) next=\"$DD_NEXT\" on the cut root — the flag that lifts this cap" ;; + *) no "(A) the cut root carries next=\"$DD_NEXT\" — expected the pasteable --doc-drift --detail=1" ;; +esac +# the next= is PASTED AS THE WHOLE ARGV, which is what "pasteable" claims, and it must cut nothing at all +if [ -n "$DD_NEXT" ]; then + # shellcheck disable=SC2086 + "$BIN" "$TMP/dd" $DD_NEXT --no-cache >"$TMP/dd_next" 2>"$TMP/dd_next.err" + if [ -z "$( attr "$TMP/dd_next" doc failed_capped )" ] && [ "$( countrows dd_next a )" = "$A_ALL" ]; then + ok "(A) next= is EXACT: pasted verbatim it emits all $A_ALL rows and cuts nothing" + else + no "(A) next=\"$DD_NEXT\" pasted verbatim still cuts rows ($( countrows dd_next a ) of $A_ALL): $( head -c 160 "$TMP/dd_next.err" )" + fi +fi + +# RE-DERIVATION — every verdict attribute, root and per-doc, byte-identical with the window removed +python3 - "$TMP/dd_def" "$TMP/dd_all" <<'PY' +import re, sys +PAGING = re.compile( r'\s(?:shown|capped|total|has_more|next_offset|offset|limit|next|at|est_tokens)="[^"]*"' + r'|\sshown_\w+="[^"]*"|\s\w+_capped="[^"]*"|\s\w+_total="[^"]*"' ) +def verdicts( path ): + body = re.sub( r'\A(?:\s*)+', '', open( path, errors="replace" ).read(), flags=re.S ) + out = [] + for m in re.finditer( r'<(doc-drift|doc)((?:\s+[\w:.-]+="[^"]*")*)\s*/?>', body ): + out.append( m.group( 1 ) + PAGING.sub( '', m.group( 2 ) ) ) + return out +a, b = verdicts( sys.argv[1] ), verdicts( sys.argv[2] ) +if a == b and len( a ) > 1: + print( " PASS (A) re-derivation: %d / verdict signatures byte-identical bare vs --detail" % len( a ) ) +else: + print( " FAIL (A) re-derivation: a verdict MOVED with the window" ) + for x, y in zip( a, b ): + if x != y: print( " bare: " + x + "\n all : " + y ); break + if len( a ) != len( b ): print( " %d rows bare, %d under --detail" % ( len( a ), len( b ) ) ) + sys.exit( 1 ) +PY +[ $? = 0 ] || fail=1 + +# =================================================================================================== +echo "=== (B) --flags: the read SITES page, the GATE rows are the answer and never do (F-07) ===" +# =================================================================================================== +run "$TMP/fl" fl_def --flags --no-cache +run "$TMP/fl" fl_all --flags --no-cache --limit=1000000 +R_DEF="$( countrows fl_def read )" +R_ALL="$( countrows fl_all read )" +G_DEF="$( countrows fl_def gate )" +G_ALL="$( countrows fl_all gate )" + +if [ "$R_ALL" -gt "$CAP_SITES" ]; then ok "(B) crossing: the uncapped run emits $R_ALL rows, past the $CAP_SITES cap" +else no "(B) crossing: only $R_ALL rows uncapped — the fixture never reaches the cap"; fi +if [ "$R_ALL" -gt "$R_DEF" ]; then ok "(B) --limit raises the per-gate site cap: $R_DEF sites bare, $R_ALL at --limit=1000000" +else no "(B) --limit does not reach the site cap ($R_DEF bare, $R_ALL at --limit=1000000)"; fi + +FL_SHOWN="$( attr "$TMP/fl_def" gate shown_reads )" +FL_CAP="$( attr "$TMP/fl_def" gate reads_capped )" +FL_NEXT="$( attr "$TMP/fl_def" flags next )" +if [ "$FL_SHOWN" = "$CAP_SITES" ] && [ "$FL_CAP" = "1" ]; then + ok "(B) disclosure: " +else + no "(B) disclosure: the cut says shown_reads=\"$FL_SHOWN\" reads_capped=\"$FL_CAP\" — F-07's silent cut is still silent" +fi +case "$FL_NEXT" in + --flags\ --limit=*) ok "(B) next=\"$FL_NEXT\" on the cut root" ;; + *) no "(B) the cut root carries next=\"$FL_NEXT\" — expected --flags --limit=N" ;; +esac + +# ANSWER: gate rows are never windowed — the count cannot move +if [ "$G_DEF" = "$G_ALL" ] && [ "$G_DEF" -gt 0 ]; then ok "(B) answer: $G_DEF rows at the default cap, the same $G_ALL uncapped — never windowed" +else no "(B) answer: $G_DEF rows bare vs $G_ALL uncapped — the ANSWER rows are being paged"; fi + +python3 - "$TMP/fl_def" "$TMP/fl_all" <<'PY' +import re, sys +PAGING = re.compile( r'\snext="[^"]*"|\sshown_\w+="[^"]*"|\s\w+_capped="[^"]*"' ) +def verdicts( path ): + body = re.sub( r'\A(?:\s*)+', '', open( path, errors="replace" ).read(), flags=re.S ) + return [ m.group( 1 ) + PAGING.sub( '', m.group( 2 ) ) + for m in re.finditer( r'<(flags|gate)((?:\s+[\w:.-]+="[^"]*")*)\s*/?>', body ) ] +a, b = verdicts( sys.argv[1] ), verdicts( sys.argv[2] ) +if a == b and len( a ) > 1: + print( " PASS (B) re-derivation: %d / verdict signatures byte-identical bare vs --limit=1000000" % len( a ) ) +else: + print( " FAIL (B) re-derivation: a --flags verdict MOVED with the window" ) + for x, y in zip( a, b ): + if x != y: print( " bare: " + x[:200] + "\n all : " + y[:200] ); break + sys.exit( 1 ) +PY +[ $? = 0 ] || fail=1 + +# =================================================================================================== +echo "=== (C) --flags --flip: six context listings page; the tests_to_run rows never do (F-07) ===" +# =================================================================================================== +run "$TMP/fl" fp_def --flags --flip=FIXTURE_WIDE_GATE --no-cache +run "$TMP/fl" fp_all --flags --flip=FIXTURE_WIDE_GATE --no-cache --limit=1000000 +P_DEF="$( countrows fp_def r )" +P_ALL="$( countrows fp_all r )" +if [ "$P_ALL" -gt "$CAP_FLIP" ]; then ok "(C) crossing: the uncapped flip emits $P_ALL rows, past the $CAP_FLIP cap" +else no "(C) crossing: only $P_ALL rows uncapped — the fixture never reaches the cap ($( head -c 200 "$TMP/fp_all.err" ))"; fi +if [ "$P_ALL" -gt "$P_DEF" ]; then ok "(C) --limit raises the flip row cap: $P_DEF rows bare, $P_ALL at --limit=1000000" +else no "(C) --limit does not reach the flip row cap ($P_DEF bare, $P_ALL uncapped)"; fi + +FP_SHOWN="$( attr "$TMP/fp_def" lights shown_r )" +FP_CAP="$( attr "$TMP/fp_def" lights r_capped )" +FP_NEXT="$( attr "$TMP/fp_def" flip next )" +if [ "$FP_SHOWN" = "$CAP_FLIP" ] && [ "$FP_CAP" = "1" ]; then + ok "(C) disclosure: " +else + no "(C) disclosure: the cut says shown_r=\"$FP_SHOWN\" r_capped=\"$FP_CAP\" — the cut is not disclosed" +fi +case "$FP_NEXT" in + --flags\ --flip=*--limit=*) ok "(C) next=\"$FP_NEXT\" on the cut root" ;; + *) no "(C) the cut root carries next=\"$FP_NEXT\" — expected --flags --flip=NAME --limit=N" ;; +esac +# ANSWER: the rows equal the tests= total at the DEFAULT cap +T_TOTAL="$( attr "$TMP/fp_def" flip tests )" +T_ROWS="$( countrows fp_def t )" +if [ "${T_TOTAL:-0}" = "$T_ROWS" ]; then ok "(C) answer: all ${T_TOTAL:-0} tests_to_run rows ride the default page — never windowed" +else no "(C) answer: tests=\"$T_TOTAL\" but $T_ROWS rows at the default cap — the ANSWER rows are being paged"; fi + +# =================================================================================================== +echo "=== (D) --situ: sections [1] and [3] page, section [2] is the answer (F-10) ===" +# =================================================================================================== +run "$TMP/st" st_def --situ=core.h --no-cache +if [ -s "$TMP/st_def" ]; then ok "(D) --situ=core.h answered (it used to REFUSE --limit outright)"; else no "(D) --situ produced nothing: $( head -c 300 "$TMP/st_def.err" )"; fi +run "$TMP/st" st_all --situ=core.h --no-cache --limit=1000000 +if [ -s "$TMP/st_all" ]; then ok "(D) --situ=core.h --limit=1000000 is HONORED (exit $?), not refused" +else no "(D) --situ still refuses --limit: $( head -c 300 "$TMP/st_all.err" )"; fi + +python3 - "$TMP/st_def" "$TMP/st_all" "$CAP_BLAST" "$CAP_PARTNER" <<'PY' +import re, sys +d, a = ( open( p, errors="replace" ).read() for p in sys.argv[1:3] ) +blastCap, partnerCap = int( sys.argv[3] ), int( sys.argv[4] ) +fail = 0 +def rows( text, marker, rowPat ): + # the indented " path (N dependent symbols)" rows of ONE section. Matched by their own shape, + # not by indentation: the counts_floor / script-gates disclosure lines are indented exactly like a row + # and counting them was this gate's own first false red. + m = re.search( re.escape( marker ) + r'.*?\n((?: \S.*\n)*)', text ) + return [ l for l in ( m.group( 1 ).splitlines() if m else [] ) if re.search( rowPat, l ) ] + +for label, marker, cap, rowPat in ( ( "[1] blast radius", " [1] blast radius", blastCap, r'\(\d+ dependent symbols\)$' ), + ( "[3] co-change", " [3] co-change", partnerCap, r'\(co-edited in \d+% of commits\)$' ) ): + dn, an = len( rows( d, marker, rowPat ) ), len( rows( a, marker, rowPat ) ) + if an <= cap: + print( " FAIL (D) crossing: %s has only %d rows uncapped — the fixture never reaches the %d cap" % ( label, an, cap ) ); fail = 1 + elif dn != cap: + print( " FAIL (D) %s printed %d rows at the default cap of %d" % ( label, dn, cap ) ); fail = 1 + elif an <= dn: + print( " FAIL (D) %s: --limit=1000000 gave %d rows, the bare run %d — --limit does not reach it" % ( label, an, dn ) ); fail = 1 + else: + print( " PASS (D) %s pages: %d rows bare (cap %d), %d at --limit=1000000" % ( label, dn, cap, an ) ) + hdr = [ l for l in d.splitlines() if l.startswith( marker ) ] + line = hdr[ 0 ] if hdr else "" + if re.search( r'shown=%d total=\d+ capped=1' % dn, line ) and "; next: --situ=core.h --limit=" in line: + print( " PASS (D) %s discloses shown=/total=/capped=1 and the exact next: invocation" % label ) + else: + print( " FAIL (D) %s header carries no shown=/total=/capped=/next: — %s" % ( label, line[:200] ) ); fail = 1 + +# ANSWER: section [2] has no cap at all — no showing-note ever, and every one of its rows is served. The +# retired cap was 25, so a fixture with more than 25 reachable test files is what makes this arm real. +for text, which in ( ( d, "bare" ), ( a, "--limit=1000000" ) ): + hdr = [ l for l in text.splitlines() if l.startswith( " [2] tests to run" ) ] + line = hdr[ 0 ] if hdr else "" + total = int( re.search( r'\((\d+)\)', line ).group( 1 ) ) if re.search( r'\((\d+)\)', line ) else 0 + served = len( rows( text, " [2] tests to run", r'^ \S' ) ) - 1 # minus the script-gates disclosure line + if total <= 25: + print( " FAIL (D) answer: only %d test rows (%s) — the fixture cannot show the retired 25-row cap is gone" % ( total, which ) ); fail = 1 + elif "showing" in line or "capped=" in line: + print( " FAIL (D) answer: section [2] is capped (%s): %s" % ( which, line[:200] ) ); fail = 1 + elif served != total: + print( " FAIL (D) answer: section [2] says %d tests and printed %d rows (%s)" % ( total, served, which ) ); fail = 1 + else: + print( " PASS (D) answer: all %d tests_to_run rows served, no cap note (%s)" % ( total, which ) ) + +# RE-DERIVATION: every counted quantity in the section headers is identical with the window removed +def counts( text ): + return ( re.findall( r'\[1\] blast radius: (\d+) symbols across (\d+) files', text ) + + re.findall( r'\[2\] tests to run \((\d+)\)', text ) + + re.findall( r'\[3\] co-change .*? \((\d+)\) window="([^"]*)" commits="(\d+)"', text ) ) +if counts( d ) == counts( a ) and counts( d ): + print( " PASS (D) re-derivation: every --situ section COUNT is identical bare vs --limit=1000000" ) +else: + print( " FAIL (D) re-derivation: a --situ count moved with the window: %r vs %r" % ( counts( d ), counts( a ) ) ); fail = 1 +sys.exit( fail ) +PY +[ $? = 0 ] || fail=1 + +# =================================================================================================== +echo "=== (E) SILENCE: a listing that FITS carries none of it, and no capped=\"0\" of this family ===" +# =================================================================================================== +run "$TMP/fit" fit_dd --doc-drift --no-cache +run "$TMP/fit" fit_fl --flags --no-cache +for f in fit_dd fit_fl; do + # the legends DEFINE this vocabulary in band, so they are stripped first — grepping the raw document + # would find the legend's own words and call a byte-neutral answer a leak. + leak="$( python3 - "$TMP/$f" <<'PYE' +import re, sys +body = re.sub( r'', '', open( sys.argv[1], errors="replace" ).read(), flags=re.S ) +m = re.search( r'\s(?:shown_\w+|\w+_capped|failed_total|next)="[^"]*"', body ) +print( m.group( 0 ).strip() if m else "" ) +PYE +)" + if [ -n "$leak" ]; then + no "(E) silence: the uncut $f document carries a paging attribute it did not need: $leak" + else + ok "(E) silence: the uncut $f document is free of the pair — byte-neutral where nothing was cut" + fi +done +# the round's rule 1, swept over EVERY document this gate produced: never a *_capped="0" of the new family, +# and never a shown_= that equals its own total. +python3 - "$TMP" <<'PY' +import os, re, sys +NEW = ( "failed", "weak", "reads", "r", "b", "hosts", "downstream", "untested", "build" ) +bad = [] +seen = 0 +for name in sorted( os.listdir( sys.argv[1] ) ): + path = os.path.join( sys.argv[1], name ) + if not os.path.isfile( path ) or name.endswith( ".err" ): continue + text = open( path, errors="replace" ).read() + body = re.sub( r'\A(?:\s*)+', '', text, flags=re.S ) + body = re.sub( r'', '', body, flags=re.S ) # the in-band legends DEFINE the vocabulary; they are not data + for noun in NEW: + for m in re.finditer( r'\s%s_capped="([^"]*)"' % noun, body ): + seen += 1 + if m.group( 1 ) != "1": bad.append( "%s: %s_capped=\"%s\" — a disclosure that fired to say nothing was cut" % ( name, noun, m.group( 1 ) ) ) + for m in re.finditer( r'<(\w[\w-]*)((?:\s+[\w:.-]+="[^"]*")*)', body ): + attrs = m.group( 2 ) + for noun in NEW: + s = re.search( r'\sshown_%s="(\d+)"' % noun, attrs ) + if not s: continue + for totalName in ( noun, noun + "_total", "n" ): + t = re.search( r'\s%s="(\d+)"' % re.escape( totalName ), attrs ) + if t and int( s.group( 1 ) ) >= int( t.group( 1 ) ): + bad.append( "%s: <%s shown_%s=\"%s\" %s=\"%s\"> — shown == total, so nothing was cut" % + ( name, m.group( 1 ), noun, s.group( 1 ), totalName, t.group( 1 ) ) ) + if t: break +if bad: + for b in bad[:6]: print( " FAIL (E) " + b ) + sys.exit( 1 ) +print( " PASS (E) rule 1 sweep: %d *_capped= of the new family across every document, every one a fired \"1\"" % seen ) +PY +[ $? = 0 ] || fail=1 + +# =================================================================================================== +echo "=== (F) MUTATION: the re-derivation comparison can go RED ===" +# =================================================================================================== +python3 - "$TMP/dd_def" <<'PY' +import re, sys +PAGING = re.compile( r'\s(?:shown|capped|total|has_more|next_offset|offset|limit|next|at|est_tokens)="[^"]*"' + r'|\sshown_\w+="[^"]*"|\s\w+_capped="[^"]*"|\s\w+_total="[^"]*"' ) +body = re.sub( r'\A(?:\s*)+', '', open( sys.argv[1], errors="replace" ).read(), flags=re.S ) +m = re.search( r'', body ) +if not m: + print( " FAIL (F) mutation: no row to mutate" ); sys.exit( 1 ) +real = m.group( 1 ) +# THE BUG THIS ARM MODELS: the verdict follows the emitted window. Rewrite drift= to the SHOWN row count, +# which is exactly what a `drift = shownCount - dated` emitter would print, and demand the comparison see it. +shown = re.search( r'\sshown_failed="(\d+)"', real ) +if not shown: + print( " FAIL (F) mutation: the default was not cut, so there is no window for a verdict to follow" ); sys.exit( 1 ) +mutated = re.sub( r'\sdrift="\d+"', ' drift="%s"' % shown.group( 1 ), real ) +if PAGING.sub( '', real ) == PAGING.sub( '', mutated ): + print( " FAIL (F) mutation: a verdict rewritten to the window's row count is INVISIBLE to the (A) comparison" ); sys.exit( 1 ) +print( " PASS (F) mutation: a drift= that followed the window IS caught by the (A) re-derivation comparison" ) +PY +[ $? = 0 ] || fail=1 + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/mcpcontractcheck.sh b/test/mcpcontractcheck.sh index 79b175cb0..e40911e86 100755 --- a/test/mcpcontractcheck.sh +++ b/test/mcpcontractcheck.sh @@ -342,6 +342,12 @@ TWIN = { # 2026-09-10: --edit-check joined the paging set (it windows its unflagged caller rows) and its twin # honors limit/offset through the same mcpPageArgs, so it is a MAPPED verb, not a CLI-only one. "--edit-check": "edit_check", + # 2026-09-10 (C1 F-07/F-10): --flags (with its --flip mode) and --situ joined the paging set when their + # row listings became windowable. Both have twins, and both twins honour limit/offset through the same + # mcpPageArgs — so they are MAPPED, not CLI-only. Note the situational twin's DEFAULT is unbounded while + # 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", } unmapped = sorted( v for v in pagingCli if v not in TWIN ) check( not unmapped, "(G) every paging CLI verb is classified twin-or-not (%s)" % ( ",".join( unmapped ) or "none unmapped" ) ) diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index 39034d0f2..470bc8db7 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -110,6 +110,24 @@ tools = json.loads( line )[ "result" ][ "tools" ] # in the tool description that says WHAT they page, without which a router reads a paging verb whose page is # undefined. Same rule as the two re-anchors above (a DECLARED argument, its bytes attributed here, in the # commit that lands it, never prose) and the same posture: 171 B of headroom, less than one more argument. +# RE-ANCHORED 2026-09-10 (C1 F-07/F-10, the listing-paging round): 41,300 -> 42,000, measured 41,830 (from +# 41,220). TWO declared optional arguments, `limit` and `offset`, on TWO verbs — `flags` and +# `situational_awareness`, which joined cli.h's honorsPaging set in the same commit (--flags windows the read +# SITES under a gate and --flip its six context listings; --situ windows its blast-radius and co-change +# sections — in both, the answer rows, the gate rows and tests_to_run, are never paged). Attributed tool by +# tool against a build of the parent commit (6afaa457), by this gate's own metric: +# flags +329 = +184 B schema (92 for the `limit` property entry, 92 for `offset`: the +# envelope plus the description arm (A/M12) obliges every declared property +# to carry) +145 B of description, the clause saying WHAT they page — this +# verb has TWO lenses (the gate table and --flip), and a router that cannot +# tell which rows page from which rows are the answer has an undefined page +# situational_awareness +281 = +184 B schema, same two entries, +97 B of description — shorter because +# the clause has one lens to describe, and it has to say the DEFAULT differs +# from the CLI's (unbounded here; the payload always served every row, so +# limit is relief for a caller who wants less, never a new cut) +# nothing else moved. +# Same rule as the three re-anchors above (a DECLARED argument, its bytes attributed here, in the commit +# that lands it, never prose) and the same posture: 170 B of headroom, less than one more argument entry. # # ── THE CEILING, DECIDED 2026-09-05 (terminality round A, lane M / M2): IT STAYS 41,000. ───────────── # Registered as an OWNER DECISION with the arithmetic, so it can be overruled with numbers rather than @@ -160,7 +178,7 @@ tools = json.loads( line )[ "result" ][ "tools" ] # TOTAL 40,986 -> 40,902 B; nothing else moved. Raw wire bytes (this gate measures json.dumps # with ensure_ascii, which spends 6 for each em dash instead of 3): 40,901 -> 40,811. # Headroom goes back UP, 14 B -> 98 B. That is item 5 below working, not a new allowance. -CEILING = 41300 +CEILING = 42000 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) diff --git a/test/morecontractcheck.sh b/test/morecontractcheck.sh index b55e4d955..ee0991873 100755 --- a/test/morecontractcheck.sh +++ b/test/morecontractcheck.sh @@ -80,7 +80,10 @@ FL="$( cat "$TMP/flags" )" # assertions go through this; a gate row and a doc row differ only in which open tag names them. element(){ printf '%s' "$1" | tr '>' '\n' | sed -n "/$2/,/<\/$3/p"; } gate(){ element "$FL" "/dev/null 2>"$TMP/k2.err"; then ok "honoring set: $v --limit=3 exits 0" @@ -631,6 +632,15 @@ TABLE = { # rows, and for the same reason: one bare shown= could not describe a listing whose other half # deliberately prints outside the window. "--edit-check": ( [ "--edit-check=escapeXml" ], "unflagged" ), + # 2026-09-10 (C1 F-07/F-10): --flags' per-gate sites and --flip's six context listings page, and + # so do --situ's blast-radius and co-change sections. Neither root carries a bare shown=/capped= — every + # cut is disclosed on the CHILD that was cut (rule 6's secondary-listing pair), so the root is uncut by + # construction and this arm's "cut nothing ⇒ the quintet must be absent" branch is the one that applies. + "--flags": ( [ "--flags" ], None ), + # --situ is the FIRST PROSE member of the honoring set: it has no XML root, so it spells the same + # 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" ), } fail = 0 missing = [ v for v in universe if v not in TABLE ] @@ -645,6 +655,21 @@ for verb in universe: if verb not in TABLE: continue args, primary = TABLE[ verb ] doc = subprocess.run( [ BIN, ROOT ] + args, capture_output=True, text=True, errors="replace" ).stdout + if primary == "PROSE": + # the prose dialect of rules 1+3: a cut section states shown=/total=/capped=1 inline, and nothing + # states capped=0. There is no window to page from, so the quintet does not apply — but the + # disclosure still has to be there and still has to be arithmetic. + cutSections = re.findall( r'shown=(\d+) total=(\d+) capped=1', doc ) + if 'capped=0' in doc: + print( f" FAIL (L) {verb}: prose report emits capped=0 — a disclosure that fired to say nothing was cut" ); fail = 1 + elif not cutSections: + print( f" .. (L) {verb}: prose report cut nothing on this corpus" ) + elif any( int( sh ) >= int( to ) for sh, to in cutSections ): + print( f" FAIL (L) {verb}: prose report says capped=1 with shown >= total: {cutSections}" ); fail = 1 + else: + checked += 1 + print( f" PASS (L) {verb}: {len(cutSections)} cut prose section(s), each shown < total with capped=1" ) + continue lead = LEAD.match( doc ) legend = lead.group( 0 ) if lead else "" body = doc[ len( legend ): ] diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 90b85c9fe..41ae6dcf5 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -38,5 +38,5 @@ safe_delete 0 b06980d52e4991e57563059d8be986bb4614779e20ccfd3790aac1ee076d2512 e verify_layer 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 5288c345d7d6e335f88b9c1daa8935db22e1dcf89c0c8bc1f6140d4cb5af0b48 graph_query 1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 bfa4319feb9dee09cfbd3991cf6fbd752297e99d14de9e75768820d2a9c8832f callers_limit 0 ab9dee52240f70055fa4d82d6b928ef52f4f5781c6a80ee39edb49805b892719 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -help_all 0 33333b4a9db33ebfd23ceb002cb54a92c03a06036f28f3d2473ec8d7d188f0c9 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +help_all 0 857f419af004218fe8ff23a6e75df6aa72b088b08feafeb9999f55b9c9a3c218 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 help_one 0 d958f81abe53aa21051deaf47dded37bf80d707a049148a6356c96e331a28da1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/regression.sh b/test/regression.sh index ca8b5e113..ebf566630 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck formaxtokenscheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck liftdisclosurecheck limitstablecheck listingpagingcheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 4e440cbdd258a13898ebbbee7b30a6be4f92648a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 20:47:20 -0400 Subject: [PATCH 41/73] =?UTF-8?q?measure(capsweep):=20the=20re-run=20on=20?= =?UTF-8?q?the=20fixed=20harness=20=E2=80=94=2051=20cap=20responses=20beco?= =?UTF-8?q?me=20117,=20and=20none=20stopped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `prepare → screen → sweep` once on the repaired harness, against a `git archive` of this branch's tip (`measured_at=da7af625`), scratch outside any git repository, ~2 h wall at load 20-58. docs/TUNING.md regenerated by `capsweep.py emit` — never hand-edited. THE SPLIT, with its recipe, which is the point of the exercise: recorded (2a444edb) this re-run denominator 195 (every row) 151 ANSWERING rows cap-sensitive invocations 59 64 ratio 30% 42% EXECUTABILITY (baseline arm): 151/195 answered | 2 unparseable | 0 unexpanded | 0 timed out | 42 refused (non-zero exit) | 0 exit 0 with 0 bytes `59 / 195` counted 44 rows that emit nothing at all into the half that "responds to NO cap". A row that emits nothing cannot respond to a cap. The 2 unparseable rows are the corpus's own 84-column harvest truncation, recorded as `unparseable` and not as `0`; the 42 refusals are the ones the corpus deliberately contains (`--callers=DoesNotExist`, `--rank-by=bogus`, …) and they belong in the corpus, not in the ratio. Both facts now travel in `screen.tsv`'s own `#` provenance lines. THE PER-CAP RESULT: caps that move >= 1 invocation 23 of 107 -> 37 of 112 (cap, invocation) response pairs 51 -> 117 caps that STOPPED moving 0 14 caps move that previously "moved nothing measurable", and nothing regressed. They fall into exactly the two families the harness was blind to: the retrieval family, which the quoting defect had left with a population of ZERO kDefaultRecallMaxTokens +119,181 B . --recall="quality delta gating exit codes" kWithGraphNodeCap +3,056 B . --for="pagerank power iteration" --with-graph kDocMentionMaxAnchors -1,741 B . --pack-task="…" --partition=3 kForCapTailSigBytes -212 B . --for="quality delta acks ledger rubber stamp" kDocMentionMaxDocsPerAnchor -157 B . --for="…chooseForRanker…" kMentionMaxSymbolsPerFile -81 B . --for="…chooseForRanker…" the git family, which measured its DEGRADED path on a corpus with no history kHandoffSymbolsPerDocFile +7,333 B . --handoff kHandoffDocRows +1,485 B . --handoff kSituBlastFilesShown -26 B . --situ kPrDefaultBudgetTokens +1 B . --pr-context kHandoffSymbolsPerCodeFile +1 B . --handoff kUnitSizeLowRiskMax +1 B . --dmm kUnitComplexityLowRiskMax +1 B . --dmm kUnitInterfacingLowRiskMax +1 B . --dmm THE +1 B ROWS ARE THE INTERESTING ONES, and they confirm a classification rather than a cap. The three `kUnit*LowRiskMax` are the BOUNDARY class: `--dmm` ECHOES their values (`low_loc=` `low_cx=` `low_params=`), so bumping one changes a digit and truncates nothing — a cap would have moved content. `kHandoffSymbolsPerCodeFile` is +1 B for the same reason on this corpus: the legend clause now names both values, and at 50 no changed CODE file in the fixture diff exceeds it, while `kHandoffSymbolsPerDocFile` moves +7,333 B of real markdown sections. The split by kind is visible in the measurement, not just in the reasoning behind it. The doc-mention caps move NEGATIVELY, independently reproducing C2 §9.2: what "moves" when they are raised is the disclosure disappearing, not content appearing. THE CORPUS HELD STILL. The file-list fingerprint was taken after `freeze_corpus` and re-checked after every one of the 114 arms: 2,336 files before, 2,336 after, no additions. The previous round's sweep wrote a 10.4 MB cache blob into the tree it was measuring and reported 103 of 108 caps moving as a result; this one is the control that says that cannot have happened here. Census note: 125 declarations / 124 names, 112 tunable, the same 12 forced to stay `constexpr` as every previous round. The count rose from 120/119 because this file's NAME filter was widened to match docs/limits_build.py's — `Shown|Hits|Per\w*File` — which is what makes `kHandoffSymbolsPerCodeFile` and `kHandoffSymbolsPerDocFile` measurable at all. Before today they were in neither `tunable.tsv` nor `constexpr_only`, and in neither LIMITS.md nor TUNING.md, while truncating output and disclosing `syms_capped="1"`. capsweepcheck: ALL PASS (21 arms, including arm (C)'s byte-for-byte regeneration of the document from these records plus the live cap census). --- bench/capsweep/screen.tsv | 396 +++++++++++++++++++------------------ bench/capsweep/sweep.tsv | 160 ++++++++++----- bench/capsweep/tunable.tsv | 7 +- docs/TUNING.md | 300 +++++++++++++++++++++------- 4 files changed, 551 insertions(+), 312 deletions(-) diff --git a/bench/capsweep/screen.tsv b/bench/capsweep/screen.tsv index 26c92f020..cdd7e332b 100644 --- a/bench/capsweep/screen.tsv +++ b/bench/capsweep/screen.tsv @@ -1,196 +1,200 @@ -# capsweep screen — columns: baseline_bytes / all_bumped_bytes / sensitive / invocation — measured_at=2a444edbedfa70ab7c6208b3eeca06213be5e8d7 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures -63 63 0 --scan-skill=skills/ripwire-orient/SKILL.md -64 64 0 --scan-skills=skills -85 85 0 --version -24432 24161 1 . -2792 2792 0 . --affected=src/graph.h -48791 48569 1 . --arch=test/archfix/rules.txt -8316 8316 0 . --around=rankGraphTeleport -83153 83153 0 . --around=rankGraphTeleport --around-depth=2 -3989 3989 0 . --around=rankGraphTeleport --around-fanout=4 -1107 1107 0 . --at=src/graph.h:3062 -0 0 0 . --at=src/graph.h:999999 -408 409 1 . --batch=$RIPWIRE_CAPSWEEP_TMP -0 0 0 . --cache=$RIPWIRE_CAPSWEEP_TMP -4357 4357 0 . --callees=rankGraphTeleport -3838 3838 0 . --callers=@src/graph.h:3062 -0 0 0 . --callers=DoesNotExist -3838 3838 0 . --callers=rankGraphTeleport -0 0 0 . --callers=rankGraphTeleport --format=bogus -4469 4469 0 . --callers=rankGraphTeleport --format=columnar -509 509 0 . --callers=rankGraphTeleport --json -861 861 0 . --callers=rankGraphTeleport --legend=compact -18656 18656 0 . --clones -7313 36519 1 . --cochange -20786 23280 1 . --cochange --cochange-groups -7347 37146 1 . --cochange --cochange-recur=2 -3402 3402 0 . --comment-coherence --limit=8 -17385 17588 1 . --communities -2152 2152 0 . --community=0 -2753 2753 0 . --connect=rankGraphTeleport,runEval,getIndex -13312 65144 1 . --context-ratio --limit=8 -2068 2068 0 . --dead-code=src -21672 21672 0 . --deps -2922 2921 1 . --dmm -3073 3076 1 . --dmm=HEAD -2961 2955 1 . --dmm=HEAD~3..HEAD -18093 18093 0 . --doc-drift -19621 19621 0 . --doc-drift --gateability -18710 18710 0 . --doc-drift --with-history -4711 4711 0 . --doctor -5052 5052 0 . --doctor --agent=claude -0 0 0 . --doctor --agent=nosuch -10831 28283 1 . --ensemble --limit=8 -1307 1307 0 . --eval -1446 1446 0 . --eval-retrieval -0 0 0 . --eval-stray=$RIPWIRE_CAPSWEEP_TMP -2004 2004 0 . --exclude=present --exclude=bench --top-k=5 -0 0 0 . --exemplar="format byte sizes for humans" -1829 1829 0 . --exercises=test/regression.sh -8077 9675 1 . --expand=compressBody --top-k=0 --compress -4936 6010 1 . --expand=rankGraphTeleport --top-k=0 -4324 5398 1 . --expand=rankGraphTeleport:1-12 --top-k=0 -11317 16927 1 . --expand=readAckRecords --top-k=0 --no-redact -0 0 0 . --export=cc.json:$RIPWIRE_CAPSWEEP_TMP -5603 38696 1 . --external-surface -5551 38641 1 . --external-surface --include-builtins -43615 43615 0 . --field-affinity -5296 5296 0 . --field-affinity=Symbol -20675 20675 0 . --flags -0 0 0 . --flags --flip=RIPWIRE_ASA -2321 2321 0 . --flags --flip=RIPWIRE_ASAN -0 0 0 . --for="cache invalidation" --format=candidates --top-k=5 -0 0 0 . --for="incremental cache invalidation when a file content hash changes" -0 0 0 . --for="pagerank power iteration" --detail=2 -0 0 0 . --for="pagerank power iteration" --with-graph -0 0 0 . --for="quality delta acks ledger rubber stamp" -0 0 0 . --for="quality delta acks ledger rubber stamp" --no-doc-mention -2173 2175 1 . --for="rankGraphTeleport" -15680 80946 1 . --for="rankGraphTeleport" --no-route -1568 1570 1 . --for="rankGraphTeleport" --signatures-only -0 0 0 . --for="tree-sitter parse of a source file" --adaptive -0 0 0 . --for="tree-sitter parse of a source file" --auto-bodies -0 0 0 . --for="tree-sitter parse of a source file" --legend=compact -0 0 0 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" -0 0 0 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-b -0 0 0 . --from-trace=- -0 0 0 . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' -34313 9155 1 . --grep=DEGRADED_PATH_ALERT -18829 8249 1 . --grep=DEGRADED_PATH_ALERT --and=cache -9365 8579 1 . --grep=DEGRADED_PATH_ALERT --grep-before=1 --grep-after=2 --limit=3 -44165 9266 1 . --grep=DEGRADED_PATH_ALERT --grep-context=1 -30648 8164 1 . --grep=DEGRADED_PATH_ALERT --grep-in=any -0 0 0 . --grep=DEGRADED_PATH_ALERT --grep=cache -37502 9613 1 . --grep=DEGRADED_PATH_ALERT --handles -27487 2339 1 . --grep=DEGRADED_PATH_ALERT --legend=compact -14863 8248 1 . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file -43969 20580 1 . --grep=deterministic -2773 4305 1 . --handoff -2818 2819 1 . --handoff --token-budget=1200 -0 0 0 . --help-task="calls(runDefaultMap, rankGraphTeleport)" -0 0 0 . --help-task="write a cheerful release announcement" -5960 5960 0 . --hotspots -0 0 0 . --hotspots --json -2022 2022 0 . --hotspots --limit=3 --offset=3 -0 0 0 . --hotspots --since="2 weeks ago" -0 0 0 . --html=$RIPWIRE_CAPSWEEP_TMP/ -2028 2036 1 . --ignore-tests --top-k=5 -7860 8975 1 . --impact=rankGraphTeleport -0 0 0 . --layout=Lang -3050 3050 0 . --layout=Symbol -1266 1266 0 . --lego=Vehicle -70889 70836 1 . --lint -66898 66845 1 . --lint --lint-ignore=naming-,cache- -0 0 0 . --lint --lint-select=cach- -50615 50615 0 . --lint --lint-select=cache- -0 0 0 . --lint --lint-select=nosuchfamily -73676 73623 1 . --lint --naming-locals -1138388 1646526 1 . --lint --sarif -0 0 0 . --lint --sarif --limit=5 -7959 7959 0 . --lint-catalog -3779 3779 0 . --lint-rules=test/lintrulesfix/rules -2368 2368 0 . --map-diff --top-k=5 -0 0 0 . --match='(if_statement) @i' -0 0 0 . --match='(if_statement)' -1923 1923 0 . --max-file-size=8K --top-k=3 -3002 3433 1 . --max-tokens=1500 -873 873 0 . --mentions=rankGraphTeleport -2827 2827 0 . --merge-scout=HEAD~2,HEAD~1 -1515 1515 0 . --mermaid -5396 5396 0 . --metrics --top-k=10 -6760 6760 0 . --naming-calibration -5311 5311 0 . --naming-consistency --limit=8 -1920 1825 1 . --no-cache --top-k=3 -1920 1825 1 . --no-ignore --top-k=3 -1920 1825 1 . --no-stable --top-k=3 -10220 10394 1 . --nonlocal-state --limit=8 -898 898 0 . --notes -1981 1981 0 . --order=stable --top-k=5 -870 870 0 . --outline=rankGraphTeleport --top-k=0 -870 870 0 . --outline=rankGraphTeleport:1-10 --top-k=0 -20582 20582 0 . --owners -12175 12194 1 . --pack-signatures --top-k=10 -0 0 0 . --pack-task="add a new output format flag to the CLI" -0 0 0 . --pack-task="add a new output format flag to the CLI" --partition=3 -65670 65729 1 . --pack-top-n=3 --top-k=0 -1426 1426 0 . --path=main,rankGraphTeleport -2547 2547 0 . --pattern='DEGRADED_PATH_ALERT(...)' -0 0 0 . --pattern='rankGraphTeleport($A, $B, $C)' -3429 3429 0 . --pattern='x' -0 0 0 . --plan-lanes --brief=$RIPWIRE_CAPSWEEP_TMP -0 0 0 . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it wit -0 0 0 . --plan-lanes=99 --task=x -3074 3074 0 . --plan-lint=test/planlintfix/wave.md -2938 2938 0 . --plan-lint=test/planlintfix/wave_ledger.md -5804 5805 1 . --pr-context -6920 6921 1 . --pr-context=HEAD~1 -8156 8156 0 . --quality-delta -17948 96189 1 . --quality-panel -0 0 0 . --query="teleport pagerank" --top-k=5 -0 0 0 . --rank-by=bogus --top-k=5 -2831 2831 0 . --rank-by=churn --top-k=5 -2931 2931 0 . --readability --limit=8 -0 0 0 . --recall="quality delta gating exit codes" -8367 8198 1 . --regex='fnv1a\w+' -3546 3543 1 . --report -0 0 0 . --run-timeout=5 -1418 1419 1 . --run-trace="cat $RIPWIRE_CAPSWEEP_TMP -0 0 0 . --run-trace="sleep 30" --run-timeout=2 -1199 1199 0 . --run-trace="true" -0 0 0 . --safe-delete=DoesNotExist -4737 4737 0 . --safe-delete=rankGraphTeleport -0 0 0 . --scip=does_not_exist.scip --callers=rankGraphTeleport -12969 12992 1 . --seams -165 165 0 . --situ -35171 35171 0 . --skipped -0 0 0 . --slice-depth=3 -5119 5119 0 . --slice=rankGraphTeleport -0 0 0 . --slice=rankGraphTeleport:nosuchvar -5147 5147 0 . --slice=rankGraphTeleport:teleport -934 934 0 . --slice=rankGraphTeleport:teleport --legend=compact -6923 6923 0 . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3 -7224 7225 1 . --slice=rankGraphTeleport:teleport --slice-flow=fwd -22906 22914 1 . --stray-content=lane -3888 3888 0 . --stray-content=lane --abi -11670369 11921048 1 . --stray-content=lane/ --plan -3164 3164 0 . --stray-content=worktree-agent- -0 0 0 . --stray-content=zzzz-no-such-ref --plan -2078 2078 0 . --test-gate -57 57 0 . --token-budget=100 -0 0 0 . --top-k=0 -4936 6010 1 . --top-k=0 --expand=rankGraphTeleport -2031 2031 0 . --top-k=5 -11779 92439 1 . --tree -4727 4727 0 . --uses=rankGraphTeleport -0 0 0 . --verify="calls(runDefaultMap, rankGraphTeleport)" -0 0 0 . --verify="contains(src/graph.h, \"no such literal anywhere\")" -0 0 0 . --verify="frobnicate(x)" -0 0 0 . --verify="unused(rankGraphTeleport)" -29578 29578 0 . --whereis=computeOnePairOverlap --with-history -18488 18488 0 . --whereis=rankGraphTeleport -8409 49298 1 . --zoom -12518 72030 1 . --zoom --zoom-levels=3 -0 0 0 $RIPWIRE_CAPSWEEP_TMP/aux/kbcor -0 0 0 skills --eval-skills=$RIPWIRE_CAPSWEEP_TMP -1958 1958 0 src test --top-k=5 -3570 3570 0 wrap claude +# capsweep screen — columns: baseline_bytes / all_bumped_bytes / sensitive / baseline_state / all_bumped_state / invocation — measured_at=da7af625543881aff54d4aedd363603f93288480 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures +# split recipe: DENOMINATOR = rows that answered under the BASELINE arm (state=ok, >0 bytes). +# A row that emits nothing cannot respond to a cap; 44 row(s) of 195 never answer and are +# recorded here but excluded from the ratio. +# cap-sensitive=64 of 151 answering (42%); 0 row(s) answer only when a cap is bumped. +63 63 0 ok ok --scan-skill=skills/ripwire-orient/SKILL.md +64 64 0 ok ok --scan-skills=skills +85 85 0 ok ok --version +24372 24345 1 ok ok . +2792 2792 0 ok ok . --affected=src/graph.h +49347 49121 1 ok ok . --arch=test/archfix/rules.txt +8316 8316 0 ok ok . --around=rankGraphTeleport +84792 84792 0 ok ok . --around=rankGraphTeleport --around-depth=2 +3989 3989 0 ok ok . --around=rankGraphTeleport --around-fanout=4 +1093 1093 0 ok ok . --at=src/graph.h:3062 +- - 0 rc=1 rc=1 . --at=src/graph.h:999999 +408 409 1 ok ok . --batch=$RIPWIRE_CAPSWEEP_TMP +- - 0 rc=1 rc=1 . --cache=$RIPWIRE_CAPSWEEP_TMP +4357 4357 0 ok ok . --callees=rankGraphTeleport +4081 4081 0 ok ok . --callers=@src/graph.h:3062 +- - 0 rc=1 rc=1 . --callers=DoesNotExist +3838 3838 0 ok ok . --callers=rankGraphTeleport +- - 0 rc=1 rc=1 . --callers=rankGraphTeleport --format=bogus +4469 4469 0 ok ok . --callers=rankGraphTeleport --format=columnar +509 509 0 ok ok . --callers=rankGraphTeleport --json +861 861 0 ok ok . --callers=rankGraphTeleport --legend=compact +18623 18623 0 ok ok . --clones +2937 2937 0 ok ok . --cochange +1993 1993 0 ok ok . --cochange --cochange-groups +2951 2951 0 ok ok . --cochange --cochange-recur=2 +3402 3402 0 ok ok . --comment-coherence --limit=8 +17674 17693 1 ok ok . --communities +2152 2152 0 ok ok . --community=0 +2753 2753 0 ok ok . --connect=rankGraphTeleport,runEval,getIndex +13314 65135 1 ok ok . --context-ratio --limit=8 +2068 2068 0 ok ok . --dead-code=src +21924 21924 0 ok ok . --deps +3045 3048 1 ok ok . --dmm +3073 3076 1 ok ok . --dmm=HEAD +- - 0 rc=1 rc=1 . --dmm=HEAD~3..HEAD +18186 18186 0 ok ok . --doc-drift +19659 19659 0 ok ok . --doc-drift --gateability +17884 17884 0 ok ok . --doc-drift --with-history +- - 0 rc=1 rc=1 . --doctor +- - 0 rc=1 rc=1 . --doctor --agent=claude +- - 0 rc=1 rc=1 . --doctor --agent=nosuch +10499 28383 1 ok ok . --ensemble --limit=8 +1306 1306 0 ok ok . --eval +1446 1446 0 ok ok . --eval-retrieval +676 676 0 ok ok . --eval-stray=$RIPWIRE_CAPSWEEP_TMP +2004 2004 0 ok ok . --exclude=present --exclude=bench --top-k=5 +2530 2530 0 ok ok . --exemplar="format byte sizes for humans" +1829 1829 0 ok ok . --exercises=test/regression.sh +8077 9675 1 ok ok . --expand=compressBody --top-k=0 --compress +4926 6033 1 ok ok . --expand=rankGraphTeleport --top-k=0 +4314 5421 1 ok ok . --expand=rankGraphTeleport:1-12 --top-k=0 +11317 16927 1 ok ok . --expand=readAckRecords --top-k=0 --no-redact +- - 0 rc=1 rc=1 . --export=cc.json:$RIPWIRE_CAPSWEEP_TMP +5611 38720 1 ok ok . --external-surface +5553 38663 1 ok ok . --external-surface --include-builtins +43277 43277 0 ok ok . --field-affinity +5343 5343 0 ok ok . --field-affinity=Symbol +20553 20553 0 ok ok . --flags +- - 0 rc=1 rc=1 . --flags --flip=RIPWIRE_ASA +2321 2321 0 ok ok . --flags --flip=RIPWIRE_ASAN +2293 2293 0 ok ok . --for="cache invalidation" --format=candidates --top-k=5 +9903 68105 1 ok ok . --for="incremental cache invalidation when a file content hash changes" +14560 66641 1 ok ok . --for="pagerank power iteration" --detail=2 +10078 65317 1 ok ok . --for="pagerank power iteration" --with-graph +10055 69178 1 ok ok . --for="quality delta acks ledger rubber stamp" +10119 69114 1 ok ok . --for="quality delta acks ledger rubber stamp" --no-doc-mention +5491 5493 1 ok ok . --for="rankGraphTeleport" +15900 81130 1 ok ok . --for="rankGraphTeleport" --no-route +2813 2815 1 ok ok . --for="rankGraphTeleport" --signatures-only +9897 67641 1 ok ok . --for="tree-sitter parse of a source file" --adaptive +15916 84924 1 ok ok . --for="tree-sitter parse of a source file" --auto-bodies +9566 67587 1 ok ok . --for="tree-sitter parse of a source file" --legend=compact +10268 68397 1 ok ok . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +- - 0 rc=1 rc=1 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-b +- - 0 rc=1 rc=1 . --from-trace=- +5246 5246 0 ok ok . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' +33683 9041 1 ok ok . --grep=DEGRADED_PATH_ALERT +18198 8135 1 ok ok . --grep=DEGRADED_PATH_ALERT --and=cache +9516 8729 1 ok ok . --grep=DEGRADED_PATH_ALERT --grep-before=1 --grep-after=2 --limit=3 +43535 9152 1 ok ok . --grep=DEGRADED_PATH_ALERT --grep-context=1 +30177 8050 1 ok ok . --grep=DEGRADED_PATH_ALERT --grep-in=any +- - 0 rc=1 rc=1 . --grep=DEGRADED_PATH_ALERT --grep=cache +36872 9499 1 ok ok . --grep=DEGRADED_PATH_ALERT --handles +26707 2075 1 ok ok . --grep=DEGRADED_PATH_ALERT --legend=compact +14233 8134 1 ok ok . --grep=DEGRADED_PATH_ALERT --not=test --grep-scope=file +44699 20641 1 ok ok . --grep=deterministic +5044 13863 1 ok ok . --handoff +4955 12290 1 ok ok . --handoff --token-budget=1200 +344 344 0 ok ok . --help-task="calls(runDefaultMap, rankGraphTeleport)" +139 139 0 ok ok . --help-task="write a cheerful release announcement" +5878 5878 0 ok ok . --hotspots +- - 0 rc=1 rc=1 . --hotspots --json +2028 2028 0 ok ok . --hotspots --limit=3 --offset=3 +286 286 0 ok ok . --hotspots --since="2 weeks ago" +- - 0 rc=1 rc=1 . --html=$RIPWIRE_CAPSWEEP_TMP/ +2028 2028 0 ok ok . --ignore-tests --top-k=5 +7860 8975 1 ok ok . --impact=rankGraphTeleport +- - 0 rc=1 rc=1 . --layout=Lang +3050 3050 0 ok ok . --layout=Symbol +1266 1266 0 ok ok . --lego=Vehicle +71025 70972 1 ok ok . --lint +66983 66930 1 ok ok . --lint --lint-ignore=naming-,cache- +- - 0 rc=1 rc=1 . --lint --lint-select=cach- +51004 51004 0 ok ok . --lint --lint-select=cache- +- - 0 rc=1 rc=1 . --lint --lint-select=nosuchfamily +73708 73655 1 ok ok . --lint --naming-locals +1162929 1675848 1 ok ok . --lint --sarif +- - 0 rc=1 rc=1 . --lint --sarif --limit=5 +8004 8004 0 ok ok . --lint-catalog +3779 3779 0 ok ok . --lint-rules=test/lintrulesfix/rules +2684 2925 1 ok ok . --map-diff --top-k=5 +17153 17153 0 ok ok . --match='(if_statement) @i' +17171 17171 0 ok ok . --match='(if_statement)' +1923 1923 0 ok ok . --max-file-size=8K --top-k=3 +3002 3433 1 ok ok . --max-tokens=1500 +873 873 0 ok ok . --mentions=rankGraphTeleport +3299 3299 0 ok ok . --merge-scout=HEAD~2,HEAD~1 +1531 1531 0 ok ok . --mermaid +5395 5395 0 ok ok . --metrics --top-k=10 +3003 3003 0 ok ok . --naming-calibration +5294 5294 0 ok ok . --naming-consistency --limit=8 +1920 1825 1 ok ok . --no-cache --top-k=3 +1920 1825 1 ok ok . --no-ignore --top-k=3 +1920 1825 1 ok ok . --no-stable --top-k=3 +9819 9993 1 ok ok . --nonlocal-state --limit=8 +898 898 0 ok ok . --notes +1981 1981 0 ok ok . --order=stable --top-k=5 +870 870 0 ok ok . --outline=rankGraphTeleport --top-k=0 +870 870 0 ok ok . --outline=rankGraphTeleport:1-10 --top-k=0 +1441 1441 0 ok ok . --owners +12256 12281 1 ok ok . --pack-signatures --top-k=10 +9480 14100 1 ok ok . --pack-task="add a new output format flag to the CLI" +24028 20379 1 ok ok . --pack-task="add a new output format flag to the CLI" --partition=3 +65670 65748 1 ok ok . --pack-top-n=3 --top-k=0 +1426 1426 0 ok ok . --path=main,rankGraphTeleport +18068 18068 0 ok ok . --pattern='DEGRADED_PATH_ALERT(...)' +2806 2806 0 ok ok . --pattern='rankGraphTeleport($A, $B, $C)' +- - 0 rc=1 rc=1 . --pattern='x' +- - 0 rc=1 rc=1 . --plan-lanes --brief=$RIPWIRE_CAPSWEEP_TMP +- - 0 unparseable: No closing quotation unparseable: No closing quotation . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it wit +- - 0 rc=1 rc=1 . --plan-lanes=99 --task=x +- - 0 rc=2 rc=2 . --plan-lint=test/planlintfix/wave.md +- - 0 rc=2 rc=2 . --plan-lint=test/planlintfix/wave_ledger.md +7947 7948 1 ok ok . --pr-context +8729 8730 1 ok ok . --pr-context=HEAD~1 +7331 7331 0 ok ok . --quality-delta +15454 82295 1 ok ok . --quality-panel +2939 2939 0 ok ok . --query="teleport pagerank" --top-k=5 +- - 0 rc=1 rc=1 . --rank-by=bogus --top-k=5 +2749 2749 0 ok ok . --rank-by=churn --top-k=5 +2931 2931 0 ok ok . --readability --limit=8 +13196 148979 1 ok ok . --recall="quality delta gating exit codes" +24849 9569 1 ok ok . --regex='fnv1a\w+' +3670 3670 0 ok ok . --report +- - 0 rc=1 rc=1 . --run-timeout=5 +- - 0 unparseable: No closing quotation unparseable: No closing quotation . --run-trace="cat $RIPWIRE_CAPSWEEP_TMP +- - 0 rc=4 rc=4 . --run-trace="sleep 30" --run-timeout=2 +1185 1185 0 ok ok . --run-trace="true" +- - 0 rc=1 rc=1 . --safe-delete=DoesNotExist +4737 4737 0 ok ok . --safe-delete=rankGraphTeleport +- - 0 rc=1 rc=1 . --scip=does_not_exist.scip --callers=rankGraphTeleport +13085 13108 1 ok ok . --seams +1705 1679 1 ok ok . --situ +36047 36047 0 ok ok . --skipped +- - 0 rc=1 rc=1 . --slice-depth=3 +5119 5119 0 ok ok . --slice=rankGraphTeleport +- - 0 rc=1 rc=1 . --slice=rankGraphTeleport:nosuchvar +5147 5147 0 ok ok . --slice=rankGraphTeleport:teleport +934 934 0 ok ok . --slice=rankGraphTeleport:teleport --legend=compact +6923 6923 0 ok ok . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3 +7224 7225 1 ok ok . --slice=rankGraphTeleport:teleport --slice-flow=fwd +- - 0 rc=1 rc=1 . --stray-content=lane +- - 0 rc=1 rc=1 . --stray-content=lane --abi +- - 0 rc=1 rc=1 . --stray-content=lane/ --plan +- - 0 rc=1 rc=1 . --stray-content=worktree-agent- +- - 0 rc=1 rc=1 . --stray-content=zzzz-no-such-ref --plan +- - 0 rc=4 rc=4 . --test-gate +- - 0 rc=3 rc=3 . --token-budget=100 +- - 0 rc=1 rc=1 . --top-k=0 +4926 6033 1 ok ok . --top-k=0 --expand=rankGraphTeleport +2031 2031 0 ok ok . --top-k=5 +11760 92319 1 ok ok . --tree +4727 4727 0 ok ok . --uses=rankGraphTeleport +2888 2888 0 ok ok . --verify="calls(runDefaultMap, rankGraphTeleport)" +2705 2705 0 ok ok . --verify="contains(src/graph.h, \"no such literal anywhere\")" +- - 0 rc=1 rc=1 . --verify="frobnicate(x)" +3375 3375 0 ok ok . --verify="unused(rankGraphTeleport)" +29563 29563 0 ok ok . --whereis=computeOnePairOverlap --with-history +18480 18480 0 ok ok . --whereis=rankGraphTeleport +8411 49130 1 ok ok . --zoom +12093 71350 1 ok ok . --zoom --zoom-levels=3 +- - 0 rc=1 rc=1 $RIPWIRE_CAPSWEEP_TMP/aux/kbcor +- - 0 rc=1 rc=1 skills --eval-skills=$RIPWIRE_CAPSWEEP_TMP +1959 1959 0 ok ok src test --top-k=5 +3592 3592 0 ok ok wrap claude diff --git a/bench/capsweep/sweep.tsv b/bench/capsweep/sweep.tsv index 5fad98a93..f7f7792dd 100644 --- a/bench/capsweep/sweep.tsv +++ b/bench/capsweep/sweep.tsv @@ -1,52 +1,118 @@ -# capsweep sweep — columns: cap / value / probe / site / default_bytes / probe_bytes / invocation — measured_at=2a444edbedfa70ab7c6208b3eeca06213be5e8d7 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures -kBatchCap 16 128 src/mcpverbs.h:4163 408 409 . --batch=$RIPWIRE_CAPSWEEP_TMP +# capsweep sweep — columns: cap / value / probe / site / default_bytes / probe_bytes / invocation — measured_at=da7af625543881aff54d4aedd363603f93288480 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures +kBatchCap 16 128 src/mcpverbs.h:4218 408 409 . --batch=$RIPWIRE_CAPSWEEP_TMP kCallHierarchyRowCap 40 320 src/pageview.h:164 7860 8975 . --impact=rankGraphTeleport -kCellsPerRowCap 12 96 src/nonlocalstate.h:116 10220 10394 . --nonlocal-state --limit=8 -kCommonNameDefThreshold 5 40 src/graph.h:244 17385 17378 . --communities -kCommonNameDefThreshold 5 40 src/graph.h:244 11779 11750 . --tree -kCommonNameDefThreshold 5 40 src/graph.h:244 8409 8457 . --zoom -kCommonNameDefThreshold 5 40 src/graph.h:244 12518 12590 . --zoom --zoom-levels=3 -kDefsPerNameCap 8 64 src/contextratio.h:94 13312 13708 . --context-ratio --limit=8 -kEnsembleFileRowCap 20 160 src/ensemble.h:108 10616 27723 . --ensemble --limit=8 -kExternalSurfaceRowCap 100 800 src/pageview.h:186 5593 38690 . --external-surface -kExternalSurfaceRowCap 100 800 src/pageview.h:186 5547 38643 . --external-surface --include-builtins -kFileRowCap 40 320 src/contextratio.h:89 13312 64424 . --context-ratio --limit=8 -kForAutoBodyBudgetBytes 6000 48000 src/serialize.h:759 15836 22117 . --for="rankGraphTeleport" --no-route -kForFileTailShownCap 24 192 src/serialize.h:814 15836 21403 . --for="rankGraphTeleport" --no-route -kForLensDefaultTopN 40 320 src/serialize.h:739 2128 2130 . --for="rankGraphTeleport" -kForLensDefaultTopN 40 320 src/serialize.h:739 15836 15628 . --for="rankGraphTeleport" --no-route -kForLensDefaultTopN 40 320 src/serialize.h:739 1523 1525 . --for="rankGraphTeleport" --signatures-only -kForPayloadBudgetBytes 7500 60000 src/serialize.h:725 15836 29649 . --for="rankGraphTeleport" --no-route -kGrepMatchedLineMaxBytes 512 4096 src/search.h:939 44342 63729 . --grep=deterministic -kLintMaxPerRule 5000 40000 src/lintrules.h:819 70938 70885 . --lint -kLintMaxPerRule 5000 40000 src/lintrules.h:819 66898 66845 . --lint --lint-ignore=naming-,cache- -kLintMaxPerRule 5000 40000 src/lintrules.h:819 73640 73587 . --lint --naming-locals -kLintMaxPerRule 5000 40000 src/lintrules.h:819 1137832 1645970 . --lint --sarif -kMaxExpandIncludes 24 192 src/serialize.h:4543 11317 11632 . --expand=readAckRecords --top-k=0 --no-redact -kMaxExpandSibs 100 800 src/serialize.h:4534 8077 9675 . --expand=compressBody --top-k=0 --compress -kMaxExpandSibs 100 800 src/serialize.h:4534 4936 6010 . --expand=rankGraphTeleport --top-k=0 -kMaxExpandSibs 100 800 src/serialize.h:4534 4324 5398 . --expand=rankGraphTeleport:1-12 --top-k=0 -kMaxExpandSibs 100 800 src/serialize.h:4534 11317 16612 . --expand=readAckRecords --top-k=0 --no-redact -kMaxExpandSibs 100 800 src/serialize.h:4534 4936 6010 . --top-k=0 --expand=rankGraphTeleport -kOrdinalWindowCap 40 320 src/ensemble.h:112 10616 10647 . --ensemble --limit=8 -kOrdinalWindowCap 40 320 src/ensemble.h:112 15876 15974 . --quality-panel -kPanelRowCap 40 320 src/qualitypanel.h:145 15876 63771 . --quality-panel -kSliceFlowDefaultDepth 8 64 src/slice.h:2093 7203 7204 . --slice=rankGraphTeleport:teleport --slice-flow=fwd -kSpecificMinLen 8 64 src/graph.h:250 24432 24403 . -kSpecificMinLen 8 64 src/graph.h:250 17385 17597 . --communities -kSpecificMinLen 8 64 src/graph.h:250 2028 2036 . --ignore-tests --top-k=5 +kCellsPerRowCap 12 96 src/nonlocalstate.h:116 9819 9993 . --nonlocal-state --limit=8 +kCommonNameDefThreshold 5 40 src/graph.h:244 17674 17669 . --communities +kCommonNameDefThreshold 5 40 src/graph.h:244 11760 11735 . --tree +kCommonNameDefThreshold 5 40 src/graph.h:244 8411 8459 . --zoom +kCommonNameDefThreshold 5 40 src/graph.h:244 12093 12165 . --zoom --zoom-levels=3 +kDefaultRecallMaxTokens 8000 64000 src/recall.h:311 13196 132377 . --recall="quality delta gating exit codes" +kDefsPerNameCap 8 64 src/contextratio.h:94 13314 13702 . --context-ratio --limit=8 +kDocMentionMaxAnchors 8 64 src/mention.h:807 9903 10059 . --for="incremental cache invalidation when a file content hash changes" +kDocMentionMaxAnchors 8 64 src/mention.h:807 14560 14562 . --for="pagerank power iteration" --detail=2 +kDocMentionMaxAnchors 8 64 src/mention.h:807 10078 10080 . --for="pagerank power iteration" --with-graph +kDocMentionMaxAnchors 8 64 src/mention.h:807 10055 10057 . --for="quality delta acks ledger rubber stamp" +kDocMentionMaxAnchors 8 64 src/mention.h:807 15900 15902 . --for="rankGraphTeleport" --no-route +kDocMentionMaxAnchors 8 64 src/mention.h:807 9897 9810 . --for="tree-sitter parse of a source file" --adaptive +kDocMentionMaxAnchors 8 64 src/mention.h:807 15916 15829 . --for="tree-sitter parse of a source file" --auto-bodies +kDocMentionMaxAnchors 8 64 src/mention.h:807 9566 9484 . --for="tree-sitter parse of a source file" --legend=compact +kDocMentionMaxAnchors 8 64 src/mention.h:807 10268 10266 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kDocMentionMaxAnchors 8 64 src/mention.h:807 9480 9593 . --pack-task="add a new output format flag to the CLI" +kDocMentionMaxAnchors 8 64 src/mention.h:807 24028 22287 . --pack-task="add a new output format flag to the CLI" --partition=3 +kDocMentionMaxDocsPerAnchor 2 34 src/mention.h:808 14560 14408 . --for="pagerank power iteration" --detail=2 +kDocMentionMaxDocsPerAnchor 2 34 src/mention.h:808 10078 9926 . --for="pagerank power iteration" --with-graph +kDocMentionMaxDocsPerAnchor 2 34 src/mention.h:808 10268 10111 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kEnsembleFileRowCap 20 160 src/ensemble.h:108 10499 27791 . --ensemble --limit=8 +kExternalSurfaceRowCap 100 800 src/pageview.h:186 5611 38720 . --external-surface +kExternalSurfaceRowCap 100 800 src/pageview.h:186 5553 38663 . --external-surface --include-builtins +kFileRowCap 40 320 src/contextratio.h:89 13314 64465 . --context-ratio --limit=8 +kForAutoBodyBudgetBytes 6000 48000 src/serialize.h:760 15900 22181 . --for="rankGraphTeleport" --no-route +kForAutoBodyBudgetBytes 6000 48000 src/serialize.h:760 15916 27182 . --for="tree-sitter parse of a source file" --auto-bodies +kForCapTailSigBytes 96 768 src/serialize.h:727 10055 9843 . --for="quality delta acks ledger rubber stamp" +kForCapTailSigBytes 96 768 src/serialize.h:727 10119 10006 . --for="quality delta acks ledger rubber stamp" --no-doc-mention +kForCapTailSigBytes 96 768 src/serialize.h:727 9897 9765 . --for="tree-sitter parse of a source file" --adaptive +kForCapTailSigBytes 96 768 src/serialize.h:727 15916 15784 . --for="tree-sitter parse of a source file" --auto-bodies +kForCapTailSigBytes 96 768 src/serialize.h:727 9566 9558 . --for="tree-sitter parse of a source file" --legend=compact +kForCapTailSigBytes 96 768 src/serialize.h:727 10268 10296 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kForCapTailSigBytes 96 768 src/serialize.h:727 9480 9544 . --pack-task="add a new output format flag to the CLI" +kForCapTailSigBytes 96 768 src/serialize.h:727 24028 24137 . --pack-task="add a new output format flag to the CLI" --partition=3 +kForFileTailShownCap 24 192 src/serialize.h:815 9903 14704 . --for="incremental cache invalidation when a file content hash changes" +kForFileTailShownCap 24 192 src/serialize.h:815 14560 16231 . --for="pagerank power iteration" --detail=2 +kForFileTailShownCap 24 192 src/serialize.h:815 10078 11749 . --for="pagerank power iteration" --with-graph +kForFileTailShownCap 24 192 src/serialize.h:815 10055 15611 . --for="quality delta acks ledger rubber stamp" +kForFileTailShownCap 24 192 src/serialize.h:815 10119 15684 . --for="quality delta acks ledger rubber stamp" --no-doc-mention +kForFileTailShownCap 24 192 src/serialize.h:815 15900 21436 . --for="rankGraphTeleport" --no-route +kForFileTailShownCap 24 192 src/serialize.h:815 9897 14662 . --for="tree-sitter parse of a source file" --adaptive +kForFileTailShownCap 24 192 src/serialize.h:815 15916 20681 . --for="tree-sitter parse of a source file" --auto-bodies +kForFileTailShownCap 24 192 src/serialize.h:815 9566 14333 . --for="tree-sitter parse of a source file" --legend=compact +kForFileTailShownCap 24 192 src/serialize.h:815 10268 15148 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kForLensDefaultTopN 40 320 src/serialize.h:740 9903 9828 . --for="incremental cache invalidation when a file content hash changes" +kForLensDefaultTopN 40 320 src/serialize.h:740 14560 14142 . --for="pagerank power iteration" --detail=2 +kForLensDefaultTopN 40 320 src/serialize.h:740 10078 9660 . --for="pagerank power iteration" --with-graph +kForLensDefaultTopN 40 320 src/serialize.h:740 10055 10103 . --for="quality delta acks ledger rubber stamp" +kForLensDefaultTopN 40 320 src/serialize.h:740 10119 9984 . --for="quality delta acks ledger rubber stamp" --no-doc-mention +kForLensDefaultTopN 40 320 src/serialize.h:740 5491 5493 . --for="rankGraphTeleport" +kForLensDefaultTopN 40 320 src/serialize.h:740 15900 16075 . --for="rankGraphTeleport" --no-route +kForLensDefaultTopN 40 320 src/serialize.h:740 2813 2815 . --for="rankGraphTeleport" --signatures-only +kForLensDefaultTopN 40 320 src/serialize.h:740 9897 9948 . --for="tree-sitter parse of a source file" --adaptive +kForLensDefaultTopN 40 320 src/serialize.h:740 15916 15965 . --for="tree-sitter parse of a source file" --auto-bodies +kForLensDefaultTopN 40 320 src/serialize.h:740 9566 9569 . --for="tree-sitter parse of a source file" --legend=compact +kForLensDefaultTopN 40 320 src/serialize.h:740 10268 10038 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 9903 18389 . --for="incremental cache invalidation when a file content hash changes" +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 14560 20545 . --for="pagerank power iteration" --detail=2 +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 10078 16063 . --for="pagerank power iteration" --with-graph +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 10055 15996 . --for="quality delta acks ledger rubber stamp" +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 10119 15859 . --for="quality delta acks ledger rubber stamp" --no-doc-mention +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 15900 30265 . --for="rankGraphTeleport" --no-route +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 9897 14781 . --for="tree-sitter parse of a source file" --adaptive +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 15916 32067 . --for="tree-sitter parse of a source file" --auto-bodies +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 9566 13971 . --for="tree-sitter parse of a source file" --legend=compact +kForPayloadBudgetBytes 7500 60000 src/serialize.h:726 10268 16466 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kGrepMatchedLineMaxBytes 512 4096 src/search.h:941 44699 63891 . --grep=deterministic +kHandoffDocRows 4 36 src/handoff.h:42 5044 6529 . --handoff +kHandoffDocRows 4 36 src/handoff.h:42 4955 4956 . --handoff --token-budget=1200 +kHandoffSymbolsPerCodeFile 50 400 src/handoff.h:79 5044 5045 . --handoff +kHandoffSymbolsPerCodeFile 50 400 src/handoff.h:79 4955 4956 . --handoff --token-budget=1200 +kHandoffSymbolsPerDocFile 12 96 src/handoff.h:80 5044 12377 . --handoff +kHandoffSymbolsPerDocFile 12 96 src/handoff.h:80 4955 12288 . --handoff --token-budget=1200 +kLintMaxPerRule 5000 40000 src/lintrules.h:821 71025 70972 . --lint +kLintMaxPerRule 5000 40000 src/lintrules.h:821 66983 66930 . --lint --lint-ignore=naming-,cache- +kLintMaxPerRule 5000 40000 src/lintrules.h:821 73708 73655 . --lint --naming-locals +kLintMaxPerRule 5000 40000 src/lintrules.h:821 1162929 1675848 . --lint --sarif +kMaxExpandIncludes 24 192 src/serialize.h:4584 11317 11632 . --expand=readAckRecords --top-k=0 --no-redact +kMaxExpandSibs 100 800 src/serialize.h:4575 8077 9675 . --expand=compressBody --top-k=0 --compress +kMaxExpandSibs 100 800 src/serialize.h:4575 4926 6033 . --expand=rankGraphTeleport --top-k=0 +kMaxExpandSibs 100 800 src/serialize.h:4575 4314 5421 . --expand=rankGraphTeleport:1-12 --top-k=0 +kMaxExpandSibs 100 800 src/serialize.h:4575 11317 16612 . --expand=readAckRecords --top-k=0 --no-redact +kMaxExpandSibs 100 800 src/serialize.h:4575 4926 6033 . --top-k=0 --expand=rankGraphTeleport +kMentionMaxSymbolsPerFile 3 35 src/mention.h:159 10268 10187 . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" +kOrdinalWindowCap 40 320 src/ensemble.h:112 10499 10786 . --ensemble --limit=8 +kOrdinalWindowCap 40 320 src/ensemble.h:112 15454 16072 . --quality-panel +kPanelRowCap 40 320 src/qualitypanel.h:145 15454 66476 . --quality-panel +kPrDefaultBudgetTokens 8000 64000 src/prcontext.h:452 7947 7948 . --pr-context +kPrDefaultBudgetTokens 8000 64000 src/prcontext.h:452 8729 8730 . --pr-context=HEAD~1 +kSituBlastFilesShown 8 64 src/situ.h:348 1705 1679 . --situ +kSliceFlowDefaultDepth 8 64 src/slice.h:2094 7224 7225 . --slice=rankGraphTeleport:teleport --slice-flow=fwd +kSpecificMinLen 8 64 src/graph.h:250 24372 24676 . +kSpecificMinLen 8 64 src/graph.h:250 17674 17600 . --communities kSpecificMinLen 8 64 src/graph.h:250 7860 7823 . --impact=rankGraphTeleport +kSpecificMinLen 8 64 src/graph.h:250 2684 2925 . --map-diff --top-k=5 kSpecificMinLen 8 64 src/graph.h:250 1920 1825 . --no-cache --top-k=3 kSpecificMinLen 8 64 src/graph.h:250 1920 1825 . --no-ignore --top-k=3 kSpecificMinLen 8 64 src/graph.h:250 1920 1825 . --no-stable --top-k=3 -kSpecificMinLen 8 64 src/graph.h:250 12175 12179 . --pack-signatures --top-k=10 -kSpecificMinLen 8 64 src/graph.h:250 65670 65729 . --pack-top-n=3 --top-k=0 -kSpecificMinLen 8 64 src/graph.h:250 3546 3543 . --report -kSpecificMinLen 8 64 src/graph.h:250 12969 12992 . --seams -kSpecificMinLen 8 64 src/graph.h:250 11779 11858 . --tree -kSpecificMinLen 8 64 src/graph.h:250 8409 8523 . --zoom -kSpecificMinLen 8 64 src/graph.h:250 12518 12689 . --zoom --zoom-levels=3 -kSymbolRowCap 40 320 src/contextratio.h:88 15876 18013 . --quality-panel -kTreeRowCap 80 640 src/pageview.h:184 11779 93405 . --tree -kZoomTopModuleCap 40 320 src/pageview.h:185 8409 49550 . --zoom -kZoomTopModuleCap 40 320 src/pageview.h:185 12518 72408 . --zoom --zoom-levels=3 +kSpecificMinLen 8 64 src/graph.h:250 65670 65748 . --pack-top-n=3 --top-k=0 +kSpecificMinLen 8 64 src/graph.h:250 13085 13108 . --seams +kSpecificMinLen 8 64 src/graph.h:250 11760 11675 . --tree +kSpecificMinLen 8 64 src/graph.h:250 8411 8525 . --zoom +kSpecificMinLen 8 64 src/graph.h:250 12093 12264 . --zoom --zoom-levels=3 +kSymbolRowCap 40 320 src/contextratio.h:88 15454 16795 . --quality-panel +kTreeRowCap 80 640 src/pageview.h:184 11760 92860 . --tree +kUnitComplexityLowRiskMax 5 40 src/dmm.h:91 3045 3046 . --dmm +kUnitComplexityLowRiskMax 5 40 src/dmm.h:91 3073 3074 . --dmm=HEAD +kUnitInterfacingLowRiskMax 2 34 src/dmm.h:92 3045 3046 . --dmm +kUnitInterfacingLowRiskMax 2 34 src/dmm.h:92 3073 3074 . --dmm=HEAD +kUnitSizeLowRiskMax 15 120 src/dmm.h:90 3045 3046 . --dmm +kUnitSizeLowRiskMax 15 120 src/dmm.h:90 3073 3074 . --dmm=HEAD +kWithGraphNodeCap 8 64 src/serialize.h:5642 10078 13134 . --for="pagerank power iteration" --with-graph +kZoomTopModuleCap 40 320 src/pageview.h:185 8411 49506 . --zoom +kZoomTopModuleCap 40 320 src/pageview.h:185 12093 71914 . --zoom --zoom-levels=3 diff --git a/bench/capsweep/tunable.tsv b/bench/capsweep/tunable.tsv index bed7a3216..9c332686a 100644 --- a/bench/capsweep/tunable.tsv +++ b/bench/capsweep/tunable.tsv @@ -1,4 +1,4 @@ -# capsweep tunable — columns: kind / value — measured_at=2a444edbedfa70ab7c6208b3eeca06213be5e8d7 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures +# capsweep tunable — columns: kind / value — measured_at=da7af625543881aff54d4aedd363603f93288480 — TSV not json: ripwire indexes .json as config keys (src/ingest_crawl.h) while .tsv is unindexed prose (src/docparse.h kUnindexedProseExts) — a harness must not enter the index it measures tunable kAtomsQueryBudget tunable kBatchCap tunable kBinarySniffCap @@ -41,6 +41,8 @@ tunable kGrepTierFileBudget tunable kHandoffCochangeRows tunable kHandoffDocRows tunable kHandoffNoteRows +tunable kHandoffSymbolsPerCodeFile +tunable kHandoffSymbolsPerDocFile tunable kImportReachRowCap tunable kLintMaxPerRule tunable kMaxAffixSet @@ -66,6 +68,7 @@ tunable kMaxWorkspaceRoots tunable kMcpEchoMaxBytes tunable kMcpPageValueMax tunable kMeasuredDigitsPricedWidth +tunable kMemberSpellingsShown tunable kMentionMaxDirectSymbols tunable kMentionMaxFiles tunable kMentionMaxRawTokens @@ -86,8 +89,10 @@ tunable kReceiptRegionBudgetBytes tunable kRecentRows tunable kRowCap tunable kRunTraceRelevantLinesCap +tunable kSelectorFilesShown tunable kSibliftMaxSeed tunable kSibliftMaxSib +tunable kSituBlastFilesShown tunable kSituPartnerFileRowsShown tunable kSituPartnerRowsShown tunable kSituTestRowsShown diff --git a/docs/TUNING.md b/docs/TUNING.md index 5355a0ccd..898bebd0c 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,18 +14,18 @@ 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 | | --- | --- | --- | --- | --- | --- | -| 120 | 119 | 107 | 12 | **23** | 84 | +| 125 | 124 | 112 | 12 | **37** | 75 | The first two columns are not the same number, and the gap is not a rounding: `src/` holds -**120 cap declarations** under **119 distinct names** (`kRowCap` declared in more than one file). The -sweep patches by NAME, so `107 + 12` accounts for the 119 NAMES — not the 120 declarations. Quoting -"108 of 120" would be wrong in both halves at once, which is exactly the shape of error a +**125 cap declarations** under **124 distinct names** (`kRowCap` declared in more than one file). The +sweep patches by NAME, so `112 + 12` accounts for the 124 NAMES — not the 125 declarations. Quoting +"113 of 125" would be wrong in both halves at once, which is exactly the shape of error a generated table exists to prevent. ## Read this ratio before the tables -**23 of 107 tunable caps move any invocation at all. 84 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 120-cap audit. Most of these constants are +**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 125-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. @@ -44,33 +44,117 @@ records them under "Refuted by re-derivation" so neither is proposed again. ## Provenance -Sizes were measured against `2a444edb`, on a corpus frozen with `git archive HEAD` at that commit. +Sizes were measured against `da7af625`, on a corpus frozen with `git archive HEAD` at that commit. The cap names, values and files below are re-read from `src/` on every run of `emit`, so a retuned or renamed cap makes `test/capsweepcheck.sh` fail rather than leaving a stale number standing. The **byte deltas are frozen** and do not re-measure themselves: they are only as current as the commit above, and a change to what a verb emits can age them without any cap moving. Re-run `prepare|screen|sweep` to refresh them. +### `kForLensDefaultTopN` = `40` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `320` — **12 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="pagerank power iteration" --detail=2` | 14560 B | 14142 B | -418 B | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 9660 B | -418 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 10038 B | -230 B | +| `. --for="rankGraphTeleport" --no-route` | 15900 B | 16075 B | +175 B | +| `. --for="quality delta acks ledger rubber stamp" --no-doc-mention` | 10119 B | 9984 B | -135 B | +| `. --for="incremental cache invalidation when a file content hash chang` | 9903 B | 9828 B | -75 B | +| `. --for="tree-sitter parse of a source file" --adaptive` | 9897 B | 9948 B | +51 B | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 15965 B | +49 B | +| `. --for="quality delta acks ledger rubber stamp"` | 10055 B | 10103 B | +48 B | +| `. --for="tree-sitter parse of a source file" --legend=compact` | 9566 B | 9569 B | +3 B | +| `. --for="rankGraphTeleport"` | 5491 B | 5493 B | +2 B | +| `. --for="rankGraphTeleport" --signatures-only` | 2813 B | 2815 B | +2 B | + ### `kSpecificMinLen` = `8` -`src/graph.h` — discloses: `importers_capped` — probe value `64` — **14 verb(s) respond** +`src/graph.h` — discloses: `importers_capped` — probe value `64` — **12 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --communities` | 17385 B | 17597 B | +212 B | -| `. --zoom --zoom-levels=3` | 12518 B | 12689 B | +171 B | -| `. --zoom` | 8409 B | 8523 B | +114 B | +| `.` | 24372 B | 24676 B | +304 B | +| `. --map-diff --top-k=5` | 2684 B | 2925 B | +241 B | +| `. --zoom --zoom-levels=3` | 12093 B | 12264 B | +171 B | +| `. --zoom` | 8411 B | 8525 B | +114 B | | `. --no-cache --top-k=3` | 1920 B | 1825 B | -95 B | | `. --no-ignore --top-k=3` | 1920 B | 1825 B | -95 B | | `. --no-stable --top-k=3` | 1920 B | 1825 B | -95 B | -| `. --tree` | 11779 B | 11858 B | +79 B | -| `. --pack-top-n=3 --top-k=0` | 65670 B | 65729 B | +59 B | +| `. --tree` | 11760 B | 11675 B | -85 B | +| `. --pack-top-n=3 --top-k=0` | 65670 B | 65748 B | +78 B | +| `. --communities` | 17674 B | 17600 B | -74 B | | `. --impact=rankGraphTeleport` | 7860 B | 7823 B | -37 B | -| `.` | 24432 B | 24403 B | -29 B | -| `. --seams` | 12969 B | 12992 B | +23 B | -| `. --ignore-tests --top-k=5` | 2028 B | 2036 B | +8 B | +| `. --seams` | 13085 B | 13108 B | +23 B | + +### `kDocMentionMaxAnchors` = `8` + +`src/mention.h` — discloses: `doc_mentions_capped`, `mention_files_capped`, `mention_syms_capped`, `mention_tokens_capped` — probe value `64` — **11 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --pack-task="add a new output format flag to the CLI" --partition=3` | 24028 B | 22287 B | -1741 B | +| `. --for="incremental cache invalidation when a file content hash chang` | 9903 B | 10059 B | +156 B | +| `. --pack-task="add a new output format flag to the CLI"` | 9480 B | 9593 B | +113 B | +| `. --for="tree-sitter parse of a source file" --adaptive` | 9897 B | 9810 B | -87 B | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 15829 B | -87 B | +| `. --for="tree-sitter parse of a source file" --legend=compact` | 9566 B | 9484 B | -82 B | +| `. --for="pagerank power iteration" --detail=2` | 14560 B | 14562 B | +2 B | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 10080 B | +2 B | +| `. --for="quality delta acks ledger rubber stamp"` | 10055 B | 10057 B | +2 B | +| `. --for="rankGraphTeleport" --no-route` | 15900 B | 15902 B | +2 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 10266 B | -2 B | + +### `kForFileTailShownCap` = `24` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `192` — **10 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="quality delta acks ledger rubber stamp" --no-doc-mention` | 10119 B | 15684 B | +5565 B | +| `. --for="quality delta acks ledger rubber stamp"` | 10055 B | 15611 B | +5556 B | +| `. --for="rankGraphTeleport" --no-route` | 15900 B | 21436 B | +5536 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 15148 B | +4880 B | +| `. --for="incremental cache invalidation when a file content hash chang` | 9903 B | 14704 B | +4801 B | +| `. --for="tree-sitter parse of a source file" --legend=compact` | 9566 B | 14333 B | +4767 B | +| `. --for="tree-sitter parse of a source file" --adaptive` | 9897 B | 14662 B | +4765 B | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 20681 B | +4765 B | +| `. --for="pagerank power iteration" --detail=2` | 14560 B | 16231 B | +1671 B | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 11749 B | +1671 B | + +### `kForPayloadBudgetBytes` = `7500` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `60000` — **10 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 32067 B | +16151 B | +| `. --for="rankGraphTeleport" --no-route` | 15900 B | 30265 B | +14365 B | +| `. --for="incremental cache invalidation when a file content hash chang` | 9903 B | 18389 B | +8486 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 16466 B | +6198 B | +| `. --for="pagerank power iteration" --detail=2` | 14560 B | 20545 B | +5985 B | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 16063 B | +5985 B | +| `. --for="quality delta acks ledger rubber stamp"` | 10055 B | 15996 B | +5941 B | +| `. --for="quality delta acks ledger rubber stamp" --no-doc-mention` | 10119 B | 15859 B | +5740 B | +| `. --for="tree-sitter parse of a source file" --adaptive` | 9897 B | 14781 B | +4884 B | +| `. --for="tree-sitter parse of a source file" --legend=compact` | 9566 B | 13971 B | +4405 B | + +### `kForCapTailSigBytes` = `96` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `768` — **8 verb(s) respond** -*12 of 14 responding invocations shown, largest |delta| first.* +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="quality delta acks ledger rubber stamp"` | 10055 B | 9843 B | -212 B | +| `. --for="tree-sitter parse of a source file" --adaptive` | 9897 B | 9765 B | -132 B | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 15784 B | -132 B | +| `. --for="quality delta acks ledger rubber stamp" --no-doc-mention` | 10119 B | 10006 B | -113 B | +| `. --pack-task="add a new output format flag to the CLI" --partition=3` | 24028 B | 24137 B | +109 B | +| `. --pack-task="add a new output format flag to the CLI"` | 9480 B | 9544 B | +64 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 10296 B | +28 B | +| `. --for="tree-sitter parse of a source file" --legend=compact` | 9566 B | 9558 B | -8 B | ### `kMaxExpandSibs` = `100` @@ -80,9 +164,9 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | --- | --- | --- | --- | | `. --expand=readAckRecords --top-k=0 --no-redact` | 11317 B | 16612 B | +5295 B | | `. --expand=compressBody --top-k=0 --compress` | 8077 B | 9675 B | +1598 B | -| `. --expand=rankGraphTeleport --top-k=0` | 4936 B | 6010 B | +1074 B | -| `. --expand=rankGraphTeleport:1-12 --top-k=0` | 4324 B | 5398 B | +1074 B | -| `. --top-k=0 --expand=rankGraphTeleport` | 4936 B | 6010 B | +1074 B | +| `. --expand=rankGraphTeleport --top-k=0` | 4926 B | 6033 B | +1107 B | +| `. --expand=rankGraphTeleport:1-12 --top-k=0` | 4314 B | 5421 B | +1107 B | +| `. --top-k=0 --expand=rankGraphTeleport` | 4926 B | 6033 B | +1107 B | ### `kCommonNameDefThreshold` = `5` @@ -90,10 +174,10 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --zoom --zoom-levels=3` | 12518 B | 12590 B | +72 B | -| `. --zoom` | 8409 B | 8457 B | +48 B | -| `. --tree` | 11779 B | 11750 B | -29 B | -| `. --communities` | 17385 B | 17378 B | -7 B | +| `. --zoom --zoom-levels=3` | 12093 B | 12165 B | +72 B | +| `. --zoom` | 8411 B | 8459 B | +48 B | +| `. --tree` | 11760 B | 11735 B | -25 B | +| `. --communities` | 17674 B | 17669 B | -5 B | ### `kLintMaxPerRule` = `5000` @@ -101,20 +185,20 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --lint --sarif` | 1137832 B | 1645970 B | +508138 B | -| `. --lint` | 70938 B | 70885 B | -53 B | -| `. --lint --lint-ignore=naming-,cache-` | 66898 B | 66845 B | -53 B | -| `. --lint --naming-locals` | 73640 B | 73587 B | -53 B | +| `. --lint --sarif` | 1162929 B | 1675848 B | +512919 B | +| `. --lint` | 71025 B | 70972 B | -53 B | +| `. --lint --lint-ignore=naming-,cache-` | 66983 B | 66930 B | -53 B | +| `. --lint --naming-locals` | 73708 B | 73655 B | -53 B | -### `kForLensDefaultTopN` = `40` +### `kDocMentionMaxDocsPerAnchor` = `2` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `320` — **3 verb(s) respond** +`src/mention.h` — discloses: `doc_mentions_capped`, `mention_files_capped`, `mention_syms_capped`, `mention_tokens_capped` — probe value `34` — **3 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --for="rankGraphTeleport" --no-route` | 15836 B | 15628 B | -208 B | -| `. --for="rankGraphTeleport"` | 2128 B | 2130 B | +2 B | -| `. --for="rankGraphTeleport" --signatures-only` | 1523 B | 1525 B | +2 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 10111 B | -157 B | +| `. --for="pagerank power iteration" --detail=2` | 14560 B | 14408 B | -152 B | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 9926 B | -152 B | ### `kExternalSurfaceRowCap` = `100` @@ -122,8 +206,44 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --external-surface` | 5593 B | 38690 B | +33097 B | -| `. --external-surface --include-builtins` | 5547 B | 38643 B | +33096 B | +| `. --external-surface --include-builtins` | 5553 B | 38663 B | +33110 B | +| `. --external-surface` | 5611 B | 38720 B | +33109 B | + +### `kForAutoBodyBudgetBytes` = `6000` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `48000` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="tree-sitter parse of a source file" --auto-bodies` | 15916 B | 27182 B | +11266 B | +| `. --for="rankGraphTeleport" --no-route` | 15900 B | 22181 B | +6281 B | + +### `kHandoffDocRows` = `4` + +`src/handoff.h` — discloses: `syms_capped` — probe value `36` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --handoff` | 5044 B | 6529 B | +1485 B | +| `. --handoff --token-budget=1200` | 4955 B | 4956 B | +1 B | + +### `kHandoffSymbolsPerCodeFile` = `50` + +`src/handoff.h` — discloses: `syms_capped` — probe value `400` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --handoff` | 5044 B | 5045 B | +1 B | +| `. --handoff --token-budget=1200` | 4955 B | 4956 B | +1 B | + +### `kHandoffSymbolsPerDocFile` = `12` + +`src/handoff.h` — discloses: `syms_capped` — probe value `96` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --handoff` | 5044 B | 12377 B | +7333 B | +| `. --handoff --token-budget=1200` | 4955 B | 12288 B | +7333 B | ### `kOrdinalWindowCap` = `40` @@ -131,8 +251,44 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --quality-panel` | 15876 B | 15974 B | +98 B | -| `. --ensemble --limit=8` | 10616 B | 10647 B | +31 B | +| `. --quality-panel` | 15454 B | 16072 B | +618 B | +| `. --ensemble --limit=8` | 10499 B | 10786 B | +287 B | + +### `kPrDefaultBudgetTokens` = `8000` + +`src/prcontext.h` — discloses: **none** — probe value `64000` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --pr-context` | 7947 B | 7948 B | +1 B | +| `. --pr-context=HEAD~1` | 8729 B | 8730 B | +1 B | + +### `kUnitComplexityLowRiskMax` = `5` + +`src/dmm.h` — discloses: **none** — probe value `40` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --dmm` | 3045 B | 3046 B | +1 B | +| `. --dmm=HEAD` | 3073 B | 3074 B | +1 B | + +### `kUnitInterfacingLowRiskMax` = `2` + +`src/dmm.h` — discloses: **none** — probe value `34` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --dmm` | 3045 B | 3046 B | +1 B | +| `. --dmm=HEAD` | 3073 B | 3074 B | +1 B | + +### `kUnitSizeLowRiskMax` = `15` + +`src/dmm.h` — discloses: **none** — probe value `120` — **2 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --dmm` | 3045 B | 3046 B | +1 B | +| `. --dmm=HEAD` | 3073 B | 3074 B | +1 B | ### `kZoomTopModuleCap` = `40` @@ -140,8 +296,8 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --zoom --zoom-levels=3` | 12518 B | 72408 B | +59890 B | -| `. --zoom` | 8409 B | 49550 B | +41141 B | +| `. --zoom --zoom-levels=3` | 12093 B | 71914 B | +59821 B | +| `. --zoom` | 8411 B | 49506 B | +41095 B | ### `kBatchCap` = `16` @@ -165,7 +321,15 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --nonlocal-state --limit=8` | 10220 B | 10394 B | +174 B | +| `. --nonlocal-state --limit=8` | 9819 B | 9993 B | +174 B | + +### `kDefaultRecallMaxTokens` = `8000` + +`src/recall.h` — discloses: **none** — probe value `64000` — **1 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --recall="quality delta gating exit codes"` | 13196 B | 132377 B | +119181 B | ### `kDefsPerNameCap` = `8` @@ -173,7 +337,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --context-ratio --limit=8` | 13312 B | 13708 B | +396 B | +| `. --context-ratio --limit=8` | 13314 B | 13702 B | +388 B | ### `kEnsembleFileRowCap` = `20` @@ -181,7 +345,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --ensemble --limit=8` | 10616 B | 27723 B | +17107 B | +| `. --ensemble --limit=8` | 10499 B | 27791 B | +17292 B | ### `kFileRowCap` = `40` @@ -189,55 +353,47 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --context-ratio --limit=8` | 13312 B | 64424 B | +51112 B | +| `. --context-ratio --limit=8` | 13314 B | 64465 B | +51151 B | -### `kForAutoBodyBudgetBytes` = `6000` +### `kGrepMatchedLineMaxBytes` = `512` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `48000` — **1 verb(s) respond** +`src/search.h` — discloses: `hits_capped` — probe value `4096` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --for="rankGraphTeleport" --no-route` | 15836 B | 22117 B | +6281 B | +| `. --grep=deterministic` | 44699 B | 63891 B | +19192 B | -### `kForFileTailShownCap` = `24` +### `kMaxExpandIncludes` = `24` `src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `192` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --for="rankGraphTeleport" --no-route` | 15836 B | 21403 B | +5567 B | - -### `kForPayloadBudgetBytes` = `7500` - -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `60000` — **1 verb(s) respond** - -| invocation | default | at probe | delta | -| --- | --- | --- | --- | -| `. --for="rankGraphTeleport" --no-route` | 15836 B | 29649 B | +13813 B | +| `. --expand=readAckRecords --top-k=0 --no-redact` | 11317 B | 11632 B | +315 B | -### `kGrepMatchedLineMaxBytes` = `512` +### `kMentionMaxSymbolsPerFile` = `3` -`src/search.h` — discloses: `hits_capped` — probe value `4096` — **1 verb(s) respond** +`src/mention.h` — discloses: `doc_mentions_capped`, `mention_files_capped`, `mention_syms_capped`, `mention_tokens_capped` — probe value `35` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --grep=deterministic` | 44342 B | 63729 B | +19387 B | +| `. --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` | 10268 B | 10187 B | -81 B | -### `kMaxExpandIncludes` = `24` +### `kPanelRowCap` = `40` -`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `192` — **1 verb(s) respond** +`src/qualitypanel.h` — discloses: `findings_capped` — probe value `320` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --expand=readAckRecords --top-k=0 --no-redact` | 11317 B | 11632 B | +315 B | +| `. --quality-panel` | 15454 B | 66476 B | +51022 B | -### `kPanelRowCap` = `40` +### `kSituBlastFilesShown` = `8` -`src/qualitypanel.h` — discloses: `findings_capped` — probe value `320` — **1 verb(s) respond** +`src/situ.h` — discloses: `tests_capped`, `untested_capped` — probe value `64` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --quality-panel` | 15876 B | 63771 B | +47895 B | +| `. --situ` | 1705 B | 1679 B | -26 B | ### `kSliceFlowDefaultDepth` = `8` @@ -245,7 +401,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --slice=rankGraphTeleport:teleport --slice-flow=fwd` | 7203 B | 7204 B | +1 B | +| `. --slice=rankGraphTeleport:teleport --slice-flow=fwd` | 7224 B | 7225 B | +1 B | ### `kSymbolRowCap` = `40` @@ -253,7 +409,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --quality-panel` | 15876 B | 18013 B | +2137 B | +| `. --quality-panel` | 15454 B | 16795 B | +1341 B | ### `kTreeRowCap` = `80` @@ -261,5 +417,13 @@ moving. Re-run `prepare|screen|sweep` to refresh them. | invocation | default | at probe | delta | | --- | --- | --- | --- | -| `. --tree` | 11779 B | 93405 B | +81626 B | +| `. --tree` | 11760 B | 92860 B | +81100 B | + +### `kWithGraphNodeCap` = `8` + +`src/serialize.h` — discloses: `calls_capped`, `inc_capped`, `sibs_capped` — probe value `64` — **1 verb(s) respond** + +| invocation | default | at probe | delta | +| --- | --- | --- | --- | +| `. --for="pagerank power iteration" --with-graph` | 10078 B | 13134 B | +3056 B | From ab0d608df89b750b5c488e1891ef8e48549fa11f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 20:28:38 -0400 Subject: [PATCH 42/73] =?UTF-8?q?perf(ingest,slice):=20the=20last=20indexe?= =?UTF-8?q?d=20child=20walks=20=E2=80=94=20bindsVisitNode,=20--slice's=20f?= =?UTF-8?q?low=20walk,=20and=20the=20three=20the=20gate=20then=20found?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ts_node_child( n, i )` and `ts_node_named_child( n, i )` are the same `ts_node__child` body: each restarts tree-sitter's child iterator at the FIRST child, so indexing C children costs O(C²) whenever the list is FLAT — which it is wherever a COMMENT can sit, because the parser splices an extra into the child array itself. Lane W2 closed the ten class-1 `ts_node_child` walks and handed over two: the one class-2 site that needed a semantic conversion, and the named-child class. bindsVisitNode's declarator loop asked for `ts_node_child( n, i )` AND `ts_node_field_name_for_child( n, i )` TWICE per index — three restarts per child. The cursor's own `ts_tree_cursor_current_field_name` is the same answer in O(1): NULL for an extra, else the non-inherited field at the child's structural index, else the name inherited through the invisible nodes above it (node.c:689 vs tree_cursor.c:657). SliceRdWalker's 15 `ts_node_named_child` loops become `forEachNamedChild` (new, in tschildren.h), each recursing frame owning its cursor. Visit order, the kSliceRdMaxIter fixpoint and every early exit are preserved exactly. Arm (B7) then stayed RED at 48x with bindsVisitNode fixed, which is how the other three were found: three more declarator loops in ingest_binds.h, cc_declHasStructuredBinding / cc_isDeclaratorField / ln_declaratorIdentifiers in ingest_metrics.h, and isDeclSiteName in ingest_sidecap.h all index the SAME `declaration`. The broad sweep that followed (15 language shapes, 16 000 comments each) refuted the class-3 rule outright: `captureBases`, `ccCallArity` and the lambda-capture walk were quadratic too. src/infra/tschildren.h now says so instead of the opposite. ISOLATION ARMS — same fixture width, walk entered vs not, RED first on W2's tip (932ce5c8): | arm | pair | pre | post | | --- | --- | ---: | ---: | | (B7) bindsVisitNode | a 16000-wide `declaration` vs the flood beside it | 6.92 s / 0.09 s = 77x | 0.09 s | | (B8) SliceRdWalker | --slice over a 16000-comment body vs the map | 2.36 s / 0.02 s = 118x | 0.04 s | | (B9) sliceWalkPreproc | --slice over a 16000-comment `#if` vs the map | 1.12 s / 0.01 s = 56x | 0.02 s | | (B10) captureBases | a 16000-wide base_class_clause vs the flood outside | 1.20 s / 0.09 s = 13x | 0.09 s | | (B11) ccCallArity | a 16000-wide argument_list vs the flood outside | 1.12 s / 0.01 s = 56x | 0.01 s | | (B12) captureLambdaShadowDecls | a 16000-wide capture list vs the flood outside | 2.36 s / 0.09 s = 26x | 0.09 s | (B9) is the arm lane W2 said could not go green until this one landed; it now does. BYTE-IDENTICAL, --no-cache, W2 tip vs this: 21/21 verb x corpus pairs (map --top-k=100000, --for, --grep, --pack-task, --lint, --dead-code, --lint --naming-locals over ripwire's tree, go 227 MB, canyonraid48/canyon) + 48/48 --slice and --slice --slice-flow=both over 24 REAL symbols, 8 per corpus + the gate's own 62 generated and 21 committed fixture x verb pairs + llvm-project. Determinism (two cold runs cmp-equal, 1 725 613 B) and xmllint --noout clean. PERF. llvm-project, ONE interleaved cold pair under the shared lock, load 28: 177.41+11.80 = 189.21 s CPU -> 172.88+9.48 = 182.36 s (-3.6%), wall 35.27 -> 36.72 s, RSS 6.20 -> 6.16 GB, map byte-identical. NO CLAIM is made on -3.6%: one pair at load 28 cannot separate it from noise, and a re-sample says why it should be small — on llvm the declarations are NARROW, so bindsVisitNode's share of busy leaves is unchanged (4.4% before, 4.5% after) and what is left under it is `ts_node_parent` (8 481 samples), a LINEAR per-node parent chain this lane did not touch. These are pathological-input defects, not llvm-scale ones. Canyon, 12 interleaved cold pairs: median +0.64%, min -1.89% — sign-flipping, no claim. Gates: childwalkscalecheck (12 scaling arms, ALL PASS, and again under ASan), padscalecheck, preprocdeadscalecheck, slice{,flow,flowsens,diff}check, lint/naminglocals/naminglens/metrics/ffi/ pattern/matchgrammar/greptier/grepfast/parsehealth/pyimportprecise/route/includeprecise/ rustimportprecise/cppqual/shadow/arity/selfcontained/nodekind/cachelint, manifest, gatecount (588, regenerated), shellgateindex, binoverride, infraport, includeangle. ASan+UBSan+LSan: exit 0, EMPTY stderr on all six 16 000-wide fixtures, --slice --slice-flow=both, --lint --naming-locals and the tree. `--quality-delta=932ce5c8..HEAD`: gating="0", exit 0 — 17 minor api-surface/new-symbol rows (cursor locals the extractor reads as symbols, plus rw::forEachNamedChild, rw::anyChildBelow, forEachDeclaratorSlot and SliceRdWalker::seqSkipping). My footprint, deliberately NOT acked. --- src/infra/tschildren.h | 62 ++++++++++- src/ingest_binds.h | 119 +++++++++++--------- src/ingest_metrics.h | 130 +++++++++------------- src/ingest_relations.h | 39 ++++--- src/ingest_sidecap.h | 18 ++-- src/slice.h | 210 ++++++++++++++++++------------------ test/childwalkscalecheck.sh | 174 +++++++++++++++++++++++++----- 7 files changed, 462 insertions(+), 290 deletions(-) diff --git a/src/infra/tschildren.h b/src/infra/tschildren.h index c3ea50037..7d9d33a72 100644 --- a/src/infra/tschildren.h +++ b/src/infra/tschildren.h @@ -10,8 +10,18 @@ // unbounded-width walk therefore collects the child list ONCE per node with a TSTreeCursor — the same // child set (named + anonymous + extras) in the same left-to-right order, O(C) total. The cursor and the // out vector are caller-owned and reused across nodes, so a warm walk allocates nothing per node. -// Bounded-shape scans (base clauses, argument lists, a declaration's declarators) keep the indexed form — -// their widths come from the grammar, not from the input file. +// +// "BOUNDED SHAPE" IS NOT A PROPERTY OF THE GRAMMAR RULE — IT IS A PROPERTY OF THE LIST, AND ALMOST NO LIST +// HAS IT. Earlier revisions of this note named base clauses, argument lists and a declaration's declarators +// as the safe indexed cases, "their widths come from the grammar, not from the input file". That was wrong, +// and every one of those three was measured quadratic on 2026-09-10 (lane W3): a `base_class_clause` with +// 16 000 comments in it is 13x the same file with the flood outside the clause, an `argument_list` 56x, a +// `declaration` 77x, a lambda capture list 26x (test/childwalkscalecheck.sh, arms B7 and B10..B12). A +// comment can sit between ANY two children of ANY node, and tree-sitter splices an extra into the child +// array it is parsing — so the width of a list is set by the FILE wherever a comment may legally appear in +// it, which is everywhere. The indexed form is right only where the node's child list cannot grow at all: +// a FIXED-INDEX probe (`ts_node_child( n, 0 )`), or a scan of a node whose children a comment cannot reach. +// If you cannot name that reason in one line, use the cursor. // // WHY IT IS ITS OWN HEADER AND NOT A SECTION OF ingest.cpp. It was one, inside ingest_metrics.h's unnamed // namespace, and that made it unreachable from the two headers that ALSO walk whole subtrees and are @@ -79,6 +89,54 @@ inline void forEachChild( TSNode n, TSTreeCursor& cur, const Fn& fn ) // A4-F2 } } +// VISIT n's NAMED children, left to right — the cursor form of a `ts_node_named_child( n, i )` loop. +// That call is the SAME `ts_node__child` body with include_anonymous=false and the same restart from the +// first child, so an indexed named-child loop is O(C²) exactly like an all-children one; and a COMMENT is +// a NAMED extra, so the flood shape above reaches this class too. Measured on the pre-change binary, +// 2026-09-10: --slice's rung-3 flow walk over a 16 000-comment definition body was 87× the plain map of +// the same file (test/childwalkscalecheck.sh arm B8). +// +// WHY FILTERING forEachChild BY ts_node_is_named REPRODUCES ts_node_named_child EXACTLY. The cursor yields +// precisely the VISIBLE children — a visible subtree, or an invisible one carrying a visible alias — which +// is `ts_node__is_relevant( child, true )`; it never yields a hidden node, it descends through it. For +// those nodes `ts_node_is_named` (alias ? alias.named : subtree.named, node.c:505) and +// `ts_node__is_relevant( child, false )` (alias ? alias.named : visible && named, node.c:109) agree term +// for term, because `visible` is already true. And a named-but-INVISIBLE node — a hidden `_rule` — is +// skipped identically by both: the cursor descends through it, and ts_node__child counts through its +// stored named child count. Same set, same order. +template< class Fn > +inline void forEachNamedChild( TSNode n, TSTreeCursor& cur, const Fn& fn ) // A4-F25: NOT noexcept — `fn` may allocate +{ + forEachChild( n, cur, [ &fn ]( TSNode child ) { return ts_node_is_named( child ) ? fn( child ) : true; } ); +} + +// TRUE when `pred` holds for any node in n's child subtree — depth-first, left to right, stopping at the +// first hit, bounded at `maxDepth` levels below n (`maxDepth < 0` = unbounded). Two walks ask exactly this +// question in exactly this shape — slicev::SliceRdWalker::hasStructureBelow ("is there a block or control +// construct below?") and cc_declHasStructuredBinding ("is there a structured_binding_declarator within 4 +// levels?") — and a second hand-written copy of the cursor-plus-recursion loop is the clone this header +// exists to prevent. `namedOnly` picks which child set: the named one (`ts_node_named_child`'s) or all. +template< class Pred > +inline bool anyChildBelow( TSNode n, int maxDepth, bool namedOnly, const Pred& pred ) +{ + if( maxDepth == 0 ) + { + return false; + } + ChildCursor cursor( n ); // this frame's own: the body recurses + bool found = false; + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + if( ( namedOnly && !ts_node_is_named( c ) ) || ( !pred( c ) && !anyChildBelow( c, maxDepth - 1, namedOnly, pred ) ) ) + { + return true; + } + found = true; + return false; + } ); + return found; +} + // APPEND n's children, left to right, to whatever `out` already holds. This is the form a DFS-STACK walk // needs: there the collected list IS the work list, so clearing it would throw the frontier away. Routing // such a walk through collectChildren instead costs it a scratch vector plus a copy of every node; the two diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 0cd2e4297..ac21b034d 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -576,22 +576,28 @@ inline std::string_view fnPtrAliasName( TSNode n, const char* t, std::string_vie { return {}; } - const std::uint32_t cc = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < cc; ++i ) - { - const char* fname = ts_node_field_name_for_child( n, i ); + // O(children): the declarator-field scan rides the cursor's own O(1) field name instead of + // `ts_node_field_name_for_child( n, i )`, which restarts the child iterator (src/infra/tschildren.h). + // A typedef's width is input-controlled — a comment sits between `typedef` and the declarator as a + // direct child like any other extra (test/childwalkscalecheck.sh, arm B7). + std::string_view found; + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) + { + const char* fname = ts_tree_cursor_current_field_name( &cursor.cur ); if( fname == nullptr || !kindIs( fname, "declarator" ) ) { - continue; + return true; } - TSNode d = ts_node_child( n, i ); + TSNode d = c; bool crossed = false; for( int guard = 0; guard < 10 && !ts_node_is_null( d ); ++guard ) { const char* dt = ts_node_type( d ); if( kindIs( dt, "type_identifier" ) ) { - return crossed ? nodeTextOf( d, src ) : std::string_view{}; + found = crossed ? nodeTextOf( d, src ) : std::string_view{}; + return false; // the indexed loop returned from here } if( kindIs( dt, "function_declarator" ) ) { @@ -604,8 +610,9 @@ inline std::string_view fnPtrAliasName( TSNode n, const char* t, std::string_vie } d = inner; } - } - return {}; + return true; + } ); + return found; } // the byte span of the DEFINITION a node sits inside — a function body or a lambda, whichever encloses it @@ -639,15 +646,16 @@ inline void collectFnBindTypeFacts( TSNode n, const char* t, std::string_view sr std::string typeName; const bool concrete = concreteWrittenType( ts_node_child_by_field_name( n, "type", 4 ), src, typeName ); const auto [ scopeStart, scopeEnd ] = enclosingDefSpan( n ); - const std::uint32_t cc = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < cc; ++i ) + // O(children), on the cursor's O(1) field name — see fnPtrAliasName above and arm B7. + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) { - const char* fname = ts_node_field_name_for_child( n, i ); + const char* fname = ts_tree_cursor_current_field_name( &cursor.cur ); if( fname == nullptr || !kindIs( fname, "declarator" ) ) { - continue; + return true; } - TSNode d = ts_node_child( n, i ); + TSNode d = c; if( kindIs( ts_node_type( d ), "init_declarator" ) ) { d = ts_node_child_by_field_name( d, "declarator", 10 ); @@ -655,10 +663,11 @@ inline void collectFnBindTypeFacts( TSNode n, const char* t, std::string_view sr const FnBindDeclShape shape = fnDeclaratorShape( d, src ); if( shape.name.empty() || ( shape.sawFn && !shape.sawPtr ) ) { - continue; // nameless, an array of pointers, or a plain function DECLARATION — no variable here + return true; // nameless, an array of pointers, or a plain function DECLARATION — no variable here } facts.push_back( { std::string( shape.name ), typeName, scopeStart, scopeEnd, concrete, shape.sawFn && shape.sawPtr } ); - } + return true; + } ); } // the gate's whole per-node collection: a declaration's variable type facts AND, from the same node, any @@ -938,12 +947,18 @@ inline void captureLambdaShadowDecls( TSNode n, std::uint32_t fileId, Lang lang, } } const TSNode caps = ts_node_child_by_field_name( n, "captures", 8 ); // lambda_capture_specifier - const std::uint32_t cc = ts_node_is_null( caps ) ? 0u : ts_node_named_child_count( caps ); - for( std::uint32_t i = 0; i < cc; ++i ) + if( ts_node_is_null( caps ) ) { - const TSNode c = ts_node_named_child( caps, i ); - const char* ct = ts_node_type( c ); - TSNode ident {}; + return; + } + // O(captures): a capture list's width is input-set like every other list — a comment run between two + // captures is spliced into its child array (src/infra/tschildren.h), and 16 000 of them measured + // 1.25 s of plain map on a one-line file before this became a cursor (arm B12). + ChildCursor capsCursor( caps ); + forEachNamedChild( caps, capsCursor.cur, [ & ]( TSNode c ) + { + const char* ct = ts_node_type( c ); + TSNode ident {}; if( kindIs( ct, "identifier" ) ) { ident = c; // simple capture `[run]` / `[&run]` (the `&` is an anonymous sibling) @@ -961,7 +976,8 @@ inline void captureLambdaShadowDecls( TSNode n, std::uint32_t fileId, Lang lang, bodySite.startByte = ts_node_start_byte( c ); pushRawBind( fileId, lang, nodeTextOf( ident, src ), std::string{}, bodySite, LocalBindKind::VarDecl, binds ); } - } + return true; + } ); } // a function DEFINITION's parameter_list, reached through its own declarator chain (`char* f(...)` / @@ -1118,18 +1134,18 @@ inline void captureFnBindDecl( TSNode n, std::uint32_t fileId, Lang lang, std::s const TSNode typeNode = ts_node_child_by_field_name( n, "type", 4 ); std::string writtenType; const bool concrete = concreteWrittenType( typeNode, src, writtenType ); - const std::uint32_t cc = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < cc; ++i ) + // O(children), on the cursor's O(1) field name — see fnPtrAliasName above and arm B7. + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) { - const char* fname = ts_node_field_name_for_child( n, i ); + const char* fname = ts_tree_cursor_current_field_name( &cursor.cur ); if( fname == nullptr || !kindIs( fname, "declarator" ) ) { - continue; + return true; } - const TSNode c = ts_node_child( n, i ); if( !kindIs( ts_node_type( c ), "init_declarator" ) ) { - continue; // no initializer → no binding fact here (a later assignment carries its own) + return true; // no initializer → no binding fact here (a later assignment carries its own) } const auto [ var, sawFnDecl, sawPtrDecl, sawRef ] = fnDeclaratorShape( ts_node_child_by_field_name( c, "declarator", 10 ), src ); const TSNode valueNode = ts_node_child_by_field_name( c, "value", 5 ); @@ -1146,7 +1162,7 @@ inline void captureFnBindDecl( TSNode n, std::uint32_t fileId, Lang lang, std::s fnUnk.push_back( { std::string( aliased ), ts_node_start_byte( n ) } ); } } - continue; + return true; } bool bareIdent = false; std::string target = fnBindTargetOf( valueNode, src, bareIdent ); @@ -1155,10 +1171,11 @@ inline void captureFnBindDecl( TSNode n, std::uint32_t fileId, Lang lang, std::s // the declarator does not itself spell a fn pointer, so only the WRITTEN TYPE can tell a bind // from a copy — and that answer needs the file's complete alias table. Hold it. pending.push_back( { std::string( var ), std::move( target ), writtenType, ts_node_start_byte( n ), concrete } ); - continue; + return true; } emitFnBind( fileId, lang, var, std::move( target ), ts_node_start_byte( n ), LocalBindKind::FnDecl, fnPos ); - } + return true; + } ); } // A5 escape guard over one `pointer_expression`: `&fn` ANYWHERE makes the variable mutable through the @@ -1337,18 +1354,17 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) // start (shadowSpanStart). const ShadowScope scope = enclosingShadowScope( n ); // a `declaration` can declare several variables (`Foo a, b;`) → one binding per declarator child. - const std::uint32_t cc = ts_node_child_count( n ); - for( std::uint32_t i = 0; i < cc; ++i ) + // O(children), not O(children³): this loop used to ask for `ts_node_child( n, i )` AND + // `ts_node_field_name_for_child( n, i )` twice, and all three restart tree-sitter's child iterator at + // the first child (src/infra/tschildren.h). A declaration's width is input-set — every comment between + // the type and the declarator is a direct child — and 16 000 of them measured 77x the identical flood + // beside the declaration (test/childwalkscalecheck.sh, arm B7, which also records why the cursor's + // O(1) `ts_tree_cursor_current_field_name` is the same answer: node.c:689 vs tree_cursor.c:657). + ChildCursor cursor( n ); + forEachChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_child( n, i ); - if( ts_node_field_name_for_child( n, i ) == nullptr ) - { - continue; - } - if( !kindIs( ts_node_field_name_for_child( n, i ), "declarator" ) ) - { - continue; - } + const char* field = ts_tree_cursor_current_field_name( &cursor.cur ); + if( field == nullptr || !kindIs( field, "declarator" ) ) { return true; } const char* ct = ts_node_type( c ); // `init_declarator`: name lives in its `declarator`, the RHS in its `value` (for auto inference). // emitDeclBinds also records the r9 VarDecl shadow fact for the declared NAME regardless of type @@ -1366,7 +1382,8 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) emitDeclBinds( fileId, lang, c, src, std::string( written ), BindSite{ ts_node_start_byte( n ), shadowSpanStart( scope, c ), scope.end }, binds ); } - } + return true; + } ); } // C++ `x = Foo();` (re-assignment to a constructor) — assignment_expression inside an expression_statement. else if( ( lang == Lang::Cpp || lang == Lang::ObjC ) && kindIs( t, "assignment_expression" ) ) @@ -1424,14 +1441,14 @@ void bindsVisitNode( BindCtx& cx, TSNode n, const char* t ) const TSNode ann = ts_node_child_by_field_name( n, "type", 4 ); // type_annotation if( !ts_node_is_null( ann ) ) { - const std::uint32_t cc = ts_node_child_count( ann ); - for( std::uint32_t i = 0; i < cc; ++i ) - { - const TSNode c = ts_node_child( ann, i ); - if( kindIs( ts_node_type( c ), "type_identifier" ) ) - { const std::uint32_t ta = ts_node_start_byte( c ), tb = ts_node_end_byte( c ); - if( ta <= tb && tb <= src.size() ) { type = finalSegment( src.substr( ta, tb - ta ) ); } break; } - } + // O(children): the scan stops at the first type_identifier, but nothing bounds how many + // comments precede one — `let x: /* … */ Foo` splices each into the annotation's child list. + ChildCursor annCursor( ann ); + forEachChild( ann, annCursor.cur, [ & ]( TSNode c ) + { if( !kindIs( ts_node_type( c ), "type_identifier" ) ) { return true; } + const std::uint32_t ta = ts_node_start_byte( c ), tb = ts_node_end_byte( c ); + if( ta <= tb && tb <= src.size() ) { type = finalSegment( src.substr( ta, tb - ta ) ); } + return false; } ); // the indexed loop's `break`: the FIRST type_identifier wins } if( type.empty() ) { diff --git a/src/ingest_metrics.h b/src/ingest_metrics.h index 10e9735c8..2ed990899 100644 --- a/src/ingest_metrics.h +++ b/src/ingest_metrics.h @@ -190,29 +190,15 @@ inline bool cc_isBooleanJoin( TSNode n, std::string_view src, Lang lang ) noexce // bounded-depth search for a structured_binding_declarator anywhere under `n` — the vendored tree-sitter-cpp // grammar nests it TWO levels below the `declaration` node (declaration -> init_declarator -> // structured_binding_declarator for `auto [a,b] = …`; verified against the vendored grammar via a parse-tree -// dump, not assumed), so a same-level-only child scan misses it. `declaration` subtrees are grammar-bounded -// (a handful of children, not attacker-widenable like a comment run), so a small depth cap (not the -// cursor/stack machinery cc_walk itself uses for the whole-function walk) is the right tool here. -inline bool cc_declHasStructuredBinding( TSNode n, int depth ) noexcept +// dump, not assumed), so a same-level-only child scan misses it. The DEPTH is grammar-bounded and still +// capped here; the WIDTH is not, and an earlier revision of this comment claimed it was. A `declaration`'s +// child list carries every comment between its type and its declarator as a direct child — extras are +// spliced into the array, not balanced by a repeat node (src/infra/tschildren.h) — so the indexed scan this +// used to be was O(C²) and measured 797 of the 2 956 child-iterator samples on a 16 000-comment declaration +// (test/childwalkscalecheck.sh, arm B7). Each frame owns its cursor: the loop body recurses. +inline bool cc_declHasStructuredBinding( TSNode n, int depth ) { - if( depth <= 0 ) - { - return false; // pathological-AST guard — declaration subtrees never legitimately need this deep - } - const std::uint32_t childCount = ts_node_child_count( n ); - for( std::uint32_t ci = 0; ci < childCount; ++ci ) - { - const TSNode child = ts_node_child( n, ci ); - if( kindIs( ts_node_type( child ), "structured_binding_declarator" ) ) - { - return true; - } - if( cc_declHasStructuredBinding( child, depth - 1 ) ) - { - return true; - } - } - return false; + return anyChildBelow( n, depth, false, []( TSNode child ) { return kindIs( ts_node_type( child ), "structured_binding_declarator" ); } ); } // Phase 1 (local-variable-indexing, docs/LOCALS_INDEXING.md): is `n` a LOCAL-VARIABLE declaration @@ -244,12 +230,26 @@ inline bool cc_isCountableLocalDecl( TSNode n, const char* t ) noexcept // `ci` of `declNode` one comma-separated declarator SLOT? The vendored grammar gives every comma-separated // declarator its own `declarator`-FIELDED direct child of the `declaration` node (`int a=1,b=2;` has TWO) — // pulled out to ONE predicate so the two counting/walking loops that need it never drift on the field name. -inline bool cc_isDeclaratorField( TSNode declNode, std::uint32_t ci ) noexcept +// Takes the child's FIELD NAME, not its index: `ts_node_field_name_for_child( declNode, ci )` restarts +// tree-sitter's child iterator at the first child on every call, so both loops below were O(C²) in a +// declaration's child count — a width a comment run sets, not the grammar (arm B7). Off a cursor, +// `ts_tree_cursor_current_field_name` answers the identical question in O(1): NULL for an extra, else the +// non-inherited field at the child's structural index, else the name inherited through the invisible nodes +// above it (third_party/deps/tree_sitter/lib/src/tree_cursor.c:657 vs node.c:689). +inline bool cc_isDeclaratorFieldName( const char* fieldName ) noexcept { - const char* fieldName = ts_node_field_name_for_child( declNode, ci ); return fieldName != nullptr && kindIs( fieldName, "declarator" ); } +// …and the ONE walk over those slots, for the same reason the predicate is shared: cc_countLocalDeclarators +// and ln_declaratorIdentifiers ask the identical question of the identical child list and must never drift. +template< class Fn > +inline void forEachDeclaratorSlot( TSNode declNode, const Fn& fn ) +{ + ChildCursor cursor( declNode ); + forEachChild( declNode, cursor.cur, [ & ]( TSNode c ) { if( cc_isDeclaratorFieldName( ts_tree_cursor_current_field_name( &cursor.cur ) ) ) { fn( c ); } return true; } ); +} + // L3 fix (2026-08-08 audit): a `declaration` node already proven countable by cc_isCountableLocalDecl can // still introduce MORE THAN ONE local — `int a=1,b=2,…,j=10;` is ONE `declaration` node holding TEN // comma-separated declarators, and counting the STATEMENT ("1") instead of each DECLARATOR undercounts on @@ -260,17 +260,10 @@ inline bool cc_isDeclaratorField( TSNode declNode, std::uint32_t ci ) noexcept // type-only statement, e.g. a local `struct Foo;` forward declaration) now correctly counts as zero rather // than the previous "1" — a declaration that names no local was never meant to be a local, and the old // per-statement count silently over-counted that shape too. -inline std::uint32_t cc_countLocalDeclarators( TSNode n ) noexcept +inline std::uint32_t cc_countLocalDeclarators( TSNode n ) { std::uint32_t count = 0; - const std::uint32_t childCount = ts_node_child_count( n ); - for( std::uint32_t ci = 0; ci < childCount; ++ci ) - { - if( cc_isDeclaratorField( n, ci ) ) - { - ++count; - } - } + forEachDeclaratorSlot( n, [ &count ]( TSNode ) { ++count; } ); return count; } @@ -1209,26 +1202,17 @@ inline void ln_extractDeclaratorIdentifiers( TSNode node, std::vector& o outIdents.push_back( node ); return; } + // Both walks below are O(children) on a cursor this frame owns (the body recurses): a declarator's own + // child list takes a comment between any two of its parts, so the width is input-set (arm B7). if( kindIs( t, "reference_declarator" ) ) { - const std::uint32_t n = ts_node_child_count( node ); - for( std::uint32_t i = 0; i < n; ++i ) - { - ln_extractDeclaratorIdentifiers( ts_node_child( node, i ), outIdents, depth - 1 ); - } + ChildCursor cursor( node ); + forEachChild( node, cursor.cur, [ & ]( TSNode c ) { ln_extractDeclaratorIdentifiers( c, outIdents, depth - 1 ); return true; } ); return; } if( kindIs( t, "init_declarator" ) || kindIs( t, "pointer_declarator" ) || kindIs( t, "array_declarator" ) ) { - const std::uint32_t n = ts_node_child_count( node ); - for( std::uint32_t i = 0; i < n; ++i ) - { - const char* fieldName = ts_node_field_name_for_child( node, i ); - if( fieldName != nullptr && kindIs( fieldName, "declarator" ) ) - { - ln_extractDeclaratorIdentifiers( ts_node_child( node, i ), outIdents, depth - 1 ); - } - } + forEachDeclaratorSlot( node, [ & ]( TSNode c ) { ln_extractDeclaratorIdentifiers( c, outIdents, depth - 1 ); } ); return; } // unrecognized wrapper (incl. structured_binding_declarator, which should never reach here — Phase 1's @@ -1244,14 +1228,7 @@ inline void ln_extractDeclaratorIdentifiers( TSNode node, std::vector& o // shared "which children are declarator slots" scan, not re-typing the field-name check. inline void ln_declaratorIdentifiers( TSNode declNode, std::vector& outIdents ) { - const std::uint32_t n = ts_node_child_count( declNode ); - for( std::uint32_t i = 0; i < n; ++i ) - { - if( cc_isDeclaratorField( declNode, i ) ) - { - ln_extractDeclaratorIdentifiers( ts_node_child( declNode, i ), outIdents, 6 ); - } - } + forEachDeclaratorSlot( declNode, [ &outIdents ]( TSNode c ) { ln_extractDeclaratorIdentifiers( c, outIdents, 6 ); } ); } // declDepth: count of `compound_statement` ancestors from `declNode` up to and including the function's @@ -1538,15 +1515,12 @@ inline std::pair callArity( TSNode nameNode, Lang lang, std TSNode args = ts_node_child_by_field_name( call, "arguments", 9 ); if( ts_node_is_null( args ) ) { - const std::uint32_t cc = ts_node_child_count( call ); - for( std::uint32_t i = 0; i < cc; ++i ) - { - const TSNode c = ts_node_child( call, i ); - const char* ct = ts_node_type( c ); - if( kindIs( ct, "argument_list" ) || kindIs( ct, "arguments" ) - || kindIs( ct, "value_arguments" ) ) // Swift - { args = c; break; } - } + ChildCursor callCursor( call ); + forEachChild( call, callCursor.cur, [ & ]( TSNode c ) + { const char* ct = ts_node_type( c ); + if( !kindIs( ct, "argument_list" ) && !kindIs( ct, "arguments" ) && !kindIs( ct, "value_arguments" ) ) { return true; } // Swift + args = c; + return false; } ); } if( ts_node_is_null( args ) ) { @@ -1554,28 +1528,24 @@ inline std::pair callArity( TSNode nameNode, Lang lang, std } // count NAMED argument children; a spread / splat / apply argument makes the count unreliable → not known. - std::uint16_t count = 0; - const std::uint32_t an = ts_node_child_count( args ); - for( std::uint32_t i = 0; i < an; ++i ) - { - const TSNode c = ts_node_child( args, i ); - if( !ts_node_is_named( c ) ) - { - continue; // skip '(' ')' ',' separators - } + // O(children): the `comment` skip below is itself the proof that this list's width is INPUT-set, and this + // counter runs once per CALL SITE, so the indexed form was O(calls x C²) — arm B11, 56x its control. + std::uint16_t count = 0; + bool unreliable = false; + ChildCursor argsCursor( args ); + forEachChild( args, argsCursor.cur, [ & ]( TSNode c ) + { if( !ts_node_is_named( c ) ) { return true; } // '(' ')' ',' separators const char* ct = ts_node_type( c ); - if( kindIs( ct, "comment" ) ) - { - continue; - } + if( kindIs( ct, "comment" ) ) { return true; } if( std::strstr( ct, "splat" ) != nullptr || std::strstr( ct, "spread" ) != nullptr || kindIs( ct, "..." ) ) { - return { 0, false }; // `f(*args)` / `f(...xs)` → unreliable + unreliable = true; // `f(*args)` / `f(...xs)` → unreliable + return false; } ++count; - } + return true; } ); (void)lang; - return { count, true }; + return unreliable ? std::pair{ 0, false } : std::pair{ count, true }; } } // namespace — ingest_metrics.h section of ingest.cpp diff --git a/src/ingest_relations.h b/src/ingest_relations.h index 7bb241c95..81a8658b8 100644 --- a/src/ingest_relations.h +++ b/src/ingest_relations.h @@ -317,11 +317,15 @@ void captureMacroBodyCalls( TSNode defineNode, std::uint32_t fileId, Lang lang, // child, collecting type nodes at both depths (Rust is a separate pass — impl Trait for T is a sibling). void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& refs ) { - const uint32_t cc = ts_node_child_count( classNode ); - for( uint32_t i = 0; i < cc; ++i ) - { - const TSNode clause = ts_node_child( classNode, i ); - const char* ct = ts_node_type( clause ); + // O(children) at all three levels. These child lists LOOK grammar-bounded — a class node's clauses, a + // clause's base types — and the earlier class-3 reasoning said so, but EXTRAS refute it: a comment run + // between two base types is spliced straight into the clause's own child array (src/infra/tschildren.h), + // and 16 000 of them measured 118× the same file with the flood outside the clause + // (test/childwalkscalecheck.sh, arm B10). Each level owns its cursor — the loops nest. + ChildCursor classCursor( classNode ); + forEachChild( classNode, classCursor.cur, [ & ]( TSNode clause ) + { + const char* ct = ts_node_type( clause ); const bool isClause = kindIs( ct, "base_class_clause" ) // C++ : public Base || kindIs( ct, "class_heritage" ) // TS/JS extends / implements (wraps clauses) || kindIs( ct, "superclasses" ) // Python class X(Base): (field) @@ -334,32 +338,33 @@ void captureBases( TSNode classNode, std::uint32_t fileId, Lang lang, std::strin || kindIs( ct, "class_interface_clause" ); // PHP implements I, J if( !isClause ) { - continue; + return true; } - const uint32_t bc = ts_node_child_count( clause ); - for( uint32_t j = 0; j < bc; ++j ) + ChildCursor clauseCursor( clause ); + forEachChild( clause, clauseCursor.cur, [ & ]( TSNode bn ) { - const TSNode bn = ts_node_child( clause, j ); - const char* bt = ts_node_type( bn ); + const char* bt = ts_node_type( bn ); if( isBaseTypeNode( bt ) ) // DIRECT: type node right under the clause { emitBaseRef( bn, fileId, lang, src, refs ); - continue; + return true; } // WRAPPED: descend ONE level into a wrapper (extends_clause / implements_clause / type_list) // and emit each type node it holds. One level is enough for every measured grammar shape. - const uint32_t wc = ts_node_child_count( bn ); - for( uint32_t w = 0; w < wc; ++w ) + ChildCursor wrapCursor( bn ); + forEachChild( bn, wrapCursor.cur, [ & ]( TSNode wn ) { - const TSNode wn = ts_node_child( bn, w ); if( isBaseTypeNode( ts_node_type( wn ) ) ) { emitBaseRef( wn, fileId, lang, src, refs ); } - } - } - } + return true; + } ); + return true; + } ); + return true; + } ); } // Rust inheritance capture (separate pass — different shape). `impl Trait for T { … }` is a top-level diff --git a/src/ingest_sidecap.h b/src/ingest_sidecap.h index f2f4dceed..559af980f 100644 --- a/src/ingest_sidecap.h +++ b/src/ingest_sidecap.h @@ -771,16 +771,22 @@ inline bool isDeclSiteName( TSNode id, TSNode parent, const char* pt ) noexcept } if( kindIs( pt, "declaration" ) ) { - const std::uint32_t cc = ts_node_child_count( parent ); - for( std::uint32_t i = 0; i < cc; ++i ) + // O(children) on the cursor's O(1) field name, and this runs PER IDENTIFIER of the declaration: + // the indexed form was O(names × C²) in a width a comment run sets (src/infra/tschildren.h, + // test/childwalkscalecheck.sh arm B7). + bool isSlot = false; + ChildCursor cursor( parent ); + forEachChild( parent, cursor.cur, [ & ]( TSNode c ) { - const char* fieldName = ts_node_field_name_for_child( parent, i ); - if( fieldName != nullptr && kindIs( fieldName, "declarator" ) && sameSpan( ts_node_child( parent, i ), id ) ) + const char* fieldName = ts_tree_cursor_current_field_name( &cursor.cur ); + if( fieldName == nullptr || !kindIs( fieldName, "declarator" ) || !sameSpan( c, id ) ) { return true; } - } - return false; + isSlot = true; + return false; + } ); + return isSlot; } if( kindIs( pt, "for_range_loop" ) ) { diff --git a/src/slice.h b/src/slice.h index f522fbb6c..7503e1f07 100644 --- a/src/slice.h +++ b/src/slice.h @@ -48,7 +48,7 @@ // */src/parser.c), not assumed from upstream docs. #include "preprocdead.h" // #62: the ONE literal `#if 0`/`#if 1` rule, shared with the ingest call-ref pass -#include "infra/tschildren.h" // ChildCursor/forEachChild — both walks below descend from the FILE root +#include "infra/tschildren.h" // ChildCursor/forEachChild/forEachNamedChild — every walk below descends from the FILE root #include "infra/sortutil.h" #include "model.h" #include "ingest.h" // sliceGrammarForFile — path → grammar, ingest's one table @@ -1320,13 +1320,22 @@ struct SliceRdWalker } // ── a block: its named children in order, each in statement position ───────────────────────── - void seq( TSNode n, SliceRdState& state ) + // O(children), not O(children²): a block's named child list is every statement AND every COMMENT + // between them — a comment is a named extra, spliced into the array itself (src/infra/tschildren.h). + // A 16 000-comment definition body measured 87× the plain map of the same file before this walk + // became a cursor (test/childwalkscalecheck.sh, arm B8). Every loop in this walker owns its own + // cursor: each of them recurses, and a nested call would reset a shared one out from under it. + void seq( TSNode n, SliceRdState& state ) { seqSkipping( n, state, TSNode{} ); } + + // the same walk with ONE named child passed over — a `case_statement`'s `value` is its label, not a + // statement. switchC calls this instead of owning a second copy of the loop. + void seqSkipping( TSNode n, SliceRdState& state, TSNode skip ) { - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount && !state.dead; ++childIndex ) - { - stmt( ts_node_named_child( n, childIndex ), state ); - } + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) + { if( state.dead ) { return false; } + if( ts_node_is_null( skip ) || !ts_node_eq( c, skip ) ) { stmt( c, state ); } + return true; } ); } bool isContainer( TSNode n ) const noexcept @@ -1335,18 +1344,9 @@ struct SliceRdWalker } // does the subtree hold a block or (cfg) a control construct? — the structure walk's "recurse or unit" test - bool hasStructureBelow( TSNode n ) const noexcept + bool hasStructureBelow( TSNode n ) const { - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) - { - const TSNode c = ts_node_named_child( n, childIndex ); - if( isContainer( c ) || ( cfg && isControlKind( c ) ) || hasStructureBelow( c ) ) - { - return true; - } - } - return false; + return anyChildBelow( n, -1, true, [ & ]( TSNode c ) { return isContainer( c ) || ( cfg && isControlKind( c ) ); } ); } bool isControlKind( TSNode n ) const noexcept @@ -1393,11 +1393,8 @@ struct SliceRdWalker unit( n, state ); return; } - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount && !state.dead; ++childIndex ) - { - structure( ts_node_named_child( n, childIndex ), state ); - } + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { if( state.dead ) { return false; } structure( c, state ); return true; } ); } // ── statement position: the control table, a block, or ONE unit (the fold rule) ────────────── @@ -1453,11 +1450,8 @@ struct SliceRdWalker } if( sliceKindIs( c, "condition_clause" ) ) { - const std::uint32_t childCount = ts_node_named_child_count( c ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) - { - unit( ts_node_named_child( c, childIndex ), state ); - } + ChildCursor cursor( c ); + forEachNamedChild( c, cursor.cur, [ & ]( TSNode part ) { unit( part, state ); return true; } ); return; } unit( c, state ); @@ -1622,37 +1616,32 @@ struct SliceRdWalker SliceRdState brk = dead(), fall = dead(); bool hasDefault = false; breakAcc.push_back( &brk ); - const TSNode body = sliceField( n, "body" ); - const std::uint32_t childCount = ts_node_is_null( body ) ? 0 : ts_node_named_child_count( body ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + const TSNode body = sliceField( n, "body" ); + if( !ts_node_is_null( body ) ) { - const TSNode c = ts_node_named_child( body, childIndex ); - if( !sliceKindIs( c, "case_statement" ) ) + ChildCursor bodyCursor( body ); + forEachNamedChild( body, bodyCursor.cur, [ & ]( TSNode c ) { - stmt( c, fall ); // a statement between cases — reachable only by fall-through - continue; - } - SliceRdState s = in; - sliceRdJoin( s, fall ); - const TSNode value = sliceField( c, "value" ); - if( ts_node_is_null( value ) ) - { - hasDefault = true; - } - else - { - unit( value, s ); - } - const std::uint32_t caseChildCount = ts_node_named_child_count( c ); - for( std::uint32_t caseChildIndex = 0; caseChildIndex < caseChildCount && !s.dead; ++caseChildIndex ) - { - const TSNode cc = ts_node_named_child( c, caseChildIndex ); - if( ts_node_is_null( value ) || !ts_node_eq( cc, value ) ) + if( !sliceKindIs( c, "case_statement" ) ) { - stmt( cc, s ); + stmt( c, fall ); // a statement between cases — reachable only by fall-through + return true; } - } - fall = s; + SliceRdState s = in; + sliceRdJoin( s, fall ); + const TSNode value = sliceField( c, "value" ); + if( ts_node_is_null( value ) ) + { + hasDefault = true; + } + else + { + unit( value, s ); + } + seqSkipping( c, s, value ); // the case's statements; its `value` label is not one + fall = s; + return true; + } ); } breakAcc.pop_back(); SliceRdState out = brk; @@ -1673,20 +1662,20 @@ struct SliceRdWalker SliceRdState tryOut = state; stmt( sliceField( n, "body" ), tryOut ); tryAcc.pop_back(); - SliceRdState out = tryOut; - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + SliceRdState out = tryOut; + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_named_child( n, childIndex ); if( !sliceKindIs( c, "catch_clause" ) ) { - continue; + return true; } SliceRdState h = handlerIn; unit( sliceField( c, "parameters" ), h ); stmt( sliceField( c, "body" ), h ); sliceRdJoin( out, h ); - } + return true; + } ); state = out; } @@ -1700,17 +1689,21 @@ struct SliceRdWalker const TSNode macroName = sliceField( n, "name" ); const TSNode alternative = sliceField( n, "alternative" ); SliceRdState bodyOut = bodyState == SlicePp::Dead ? dead() : state; - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount && !bodyOut.dead; ++childIndex ) + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_named_child( n, childIndex ); - const bool skip = ( !ts_node_is_null( condition ) && ts_node_eq( c, condition ) ) || ( !ts_node_is_null( macroName ) && ts_node_eq( c, macroName ) ) - || ( !ts_node_is_null( alternative ) && ts_node_eq( c, alternative ) ); + if( bodyOut.dead ) + { + return false; + } + const bool skip = ( !ts_node_is_null( condition ) && ts_node_eq( c, condition ) ) || ( !ts_node_is_null( macroName ) && ts_node_eq( c, macroName ) ) + || ( !ts_node_is_null( alternative ) && ts_node_eq( c, alternative ) ); if( !skip ) { stmt( c, bodyOut ); } - } + return true; + } ); SliceRdState altOut = dead(); if( !ts_node_is_null( alternative ) ) { @@ -1760,11 +1753,10 @@ struct SliceRdWalker } if( sliceKindIs( n, "with_statement" ) ) { - const TSNode body = sliceField( n, "body" ); - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + const TSNode body = sliceField( n, "body" ); + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_named_child( n, childIndex ); if( !ts_node_is_null( body ) && ts_node_eq( c, body ) ) { stmt( c, state ); @@ -1773,7 +1765,8 @@ struct SliceRdWalker { unit( c, state ); // the with_clause: the context expressions, then the `as` targets } - } + return true; + } ); return true; } if( sliceKindIs( n, "match_statement" ) ) @@ -1809,11 +1802,10 @@ struct SliceRdWalker stmt( sliceField( n, "consequence" ), t ); sliceRdJoin( out, t ); } - bool hasElse = false; - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + bool hasElse = false; + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_named_child( n, childIndex ); if( sliceKindIs( c, "elif_clause" ) ) { unit( sliceField( c, "condition" ), falseS ); @@ -1828,7 +1820,8 @@ struct SliceRdWalker sliceRdJoin( out, t ); hasElse = true; } - } + return true; + } ); if( !hasElse ) { sliceRdJoin( out, falseS ); @@ -1846,19 +1839,17 @@ struct SliceRdWalker SliceRdState tryOut = state; stmt( sliceField( n, "body" ), tryOut ); tryAcc.pop_back(); - SliceRdState handlersOut = dead(), normalOut = tryOut; - TSNode finallyClause{}; - const std::uint32_t childCount = ts_node_named_child_count( n ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + SliceRdState handlersOut = dead(), normalOut = tryOut; + TSNode finallyClause{}; + ChildCursor cursor( n ); + forEachNamedChild( n, cursor.cur, [ & ]( TSNode c ) { - const TSNode c = ts_node_named_child( n, childIndex ); if( sliceKindIs( c, "except_clause" ) || sliceKindIs( c, "except_group_clause" ) ) { - SliceRdState h = handlerIn; - const std::uint32_t partCount = ts_node_named_child_count( c ); - for( std::uint32_t partIndex = 0; partIndex < partCount; ++partIndex ) + SliceRdState h = handlerIn; + ChildCursor partCursor( c ); + forEachNamedChild( c, partCursor.cur, [ & ]( TSNode part ) { - const TSNode part = ts_node_named_child( c, partIndex ); if( sliceKindIs( part, "block" ) ) { stmt( part, h ); @@ -1867,7 +1858,8 @@ struct SliceRdWalker { unit( part, h ); // the exception expression and the `as` name } - } + return true; + } ); sliceRdJoin( handlersOut, h ); } else if( sliceKindIs( c, "else_clause" ) ) @@ -1878,7 +1870,8 @@ struct SliceRdWalker { finallyClause = c; } - } + return true; + } ); if( ts_node_is_null( finallyClause ) ) { sliceRdJoin( normalOut, handlersOut ); @@ -1898,29 +1891,32 @@ struct SliceRdWalker void matchPy( TSNode n, SliceRdState& state ) { unit( sliceField( n, "subject" ), state ); - const TSNode body = sliceField( n, "body" ); - SliceRdState out = state; - const std::uint32_t childCount = ts_node_is_null( body ) ? 0 : ts_node_named_child_count( body ); - for( std::uint32_t childIndex = 0; childIndex < childCount; ++childIndex ) + const TSNode body = sliceField( n, "body" ); + SliceRdState out = state; + if( !ts_node_is_null( body ) ) { - const TSNode c = ts_node_named_child( body, childIndex ); - if( !sliceKindIs( c, "case_clause" ) ) + ChildCursor bodyCursor( body ); + forEachNamedChild( body, bodyCursor.cur, [ & ]( TSNode c ) { - continue; - } - SliceRdState s = state; - const TSNode consequence = sliceField( c, "consequence" ); - const std::uint32_t partCount = ts_node_named_child_count( c ); - for( std::uint32_t partIndex = 0; partIndex < partCount; ++partIndex ) - { - const TSNode part = ts_node_named_child( c, partIndex ); - if( ts_node_is_null( consequence ) || !ts_node_eq( part, consequence ) ) + if( !sliceKindIs( c, "case_clause" ) ) { - unit( part, s ); // patterns (capture defs) and the guard + return true; } - } - stmt( consequence, s ); - sliceRdJoin( out, s ); + SliceRdState s = state; + const TSNode consequence = sliceField( c, "consequence" ); + ChildCursor partCursor( c ); + forEachNamedChild( c, partCursor.cur, [ & ]( TSNode part ) + { + if( ts_node_is_null( consequence ) || !ts_node_eq( part, consequence ) ) + { + unit( part, s ); // patterns (capture defs) and the guard + } + return true; + } ); + stmt( consequence, s ); + sliceRdJoin( out, s ); + return true; + } ); } state = out; } diff --git a/test/childwalkscalecheck.sh b/test/childwalkscalecheck.sh index 56ec5be9d..2f4f32bfd 100755 --- a/test/childwalkscalecheck.sh +++ b/test/childwalkscalecheck.sh @@ -1,6 +1,9 @@ #!/usr/bin/env bash -# childwalkscalecheck.sh — the SCALING gate for the ten remaining unbounded indexed child walks, and the -# answers each of them must still produce. +# childwalkscalecheck.sh — the SCALING gate for every unbounded indexed child walk left after audit P1-0, +# and the answers each of them must still produce. Lane W2 wrote arms (B1..B6) for the ten class-1 +# `ts_node_child( n, i )` walks; lane W3 added (B7..B9) for the last two — bindsVisitNode's index-keyed +# field lookup and --slice`s rung-3 flow walk (`ts_node_named_child`, the same defect with +# include_anonymous=false) — which together closed the class. # # bash test/childwalkscalecheck.sh # build/ripwire # bash test/childwalkscalecheck.sh .ripwire_pre # the RED run (pre-change binary, indexed walks) @@ -37,7 +40,7 @@ # converted wide walk that produced the answer: the slice's vars, the grep hit, the pattern match, # the extern "C" symbol row, `parse_degraded="1"` for the recovered file, and the eight # `naming-underscore` LOCAL rows that only --naming-locals can reach. Plus determinism. -# (B1..B6) SCALING — user CPU, every arm an ISOLATION pair: the SAME fixture width with the walk +# (B1..B12) SCALING — user CPU, every arm an ISOLATION pair: the SAME fixture width with the walk # ENTERED and NOT entered (--slice vs the plain map, --grep vs the plain map, --pattern vs the plain # map, --lint --naming-locals vs --lint, an error token present vs absent, `extern "C"` present vs # absent). An isolation pair names ONE walk instead of saying "the verb got slower", and it cannot @@ -51,38 +54,60 @@ # small arm cannot manufacture a large one. # # NOT CONVERTED, AND WHY (the rest of the audit P1-0 follow-up table, whose class 1 this gate closes): -# * src/slice.h sliceWalkPreproc IS converted, but has NO scaling arm here and cannot get one YET. Two -# measurements say why. (i) Its natural isolation control — the identical comment flood inside the -# same definition with the `#if` removed — routes through sliceWalk's own child loop, which was -# quadratic too, so the pair reads 0.98x on the pre-change binary AND 0.98x on the fixed one: it can -# never go red. (ii) A 1k->16k ratio cannot go GREEN, because --slice's rung-3 flow walk -# (SliceRdWalker, ~11 `ts_node_named_child` loops in this same file) is a LARGER quadratic on the same -# path and this lane does not own it: --slice over a 16 000-comment definition went 2.38 s -> 1.21 s -# here (this lane's half), and the residual 1.21 s is `ts_node_named_child` in a `sample` of the fixed -# binary — dominant even on a fixture whose slice resolves ZERO vars. `ts_node_named_child` is the -# SAME `ts_node__child` body with include_anonymous=false and the same restart, and a comment is a -# NAMED extra, so the whole 55-site named-child class has this defect; it is the next lane's, whole, -# rather than half-converted here. sliceWalkPreproc's conversion is gated by arms (A2) and (C). +# * src/slice.h sliceWalkPreproc HAS a scaling arm now — (B9) — and could not before. Two measurements +# said why, and the second one is what changed. (i) Its natural isolation control (the identical flood +# with the `#if` removed) routes through sliceWalk's own child loop, so that pair read 0.98x on BOTH +# binaries and could never go red; the control used instead is the PLAIN MAP of the same file, which +# enters neither slice walk. (ii) The arm could not go GREEN while --slice's rung-3 flow walk +# (SliceRdWalker, 15 `ts_node_named_child` loops in this same file) was a LARGER quadratic on the same +# path: --slice over a 16 000-comment definition was 2.38 s before lane W2, 1.21 s after it, and a +# `sample` of the W2 binary put that residual 1.21 s in `ts_node_named_child` even on a fixture whose +# slice resolves ZERO vars. Lane W3 converted that walk (B8), which is what lets (B9) go green. # * src/pattern.h smallestContaining / snapshotNode ARE converted, but have NO arm here and cannot get # one: both index the children of the PATTERN's parse tree, and pattern.h:78 caps a pattern at # kMaxPatternBytes = 4096 — every path in, --pattern and --lint-rules alike, goes through that one # check (pattern.h:683). 4096 bytes is ~2 000 children, i.e. ~2e6 iterator steps, ~1 ms. The cap is # why the site was never hot; the conversion is for uniformity and is covered by arm (C). -# * src/ingest_binds.h:1343 (bindsVisitNode) keeps the indexed form: its body needs the INDEX for -# `ts_node_field_name_for_child( n, i )`, which is itself index-based, so collecting the children -# would leave the loop quadratic in the field lookup. The cursor's own O(1) -# `ts_tree_cursor_current_field_name` is the real fix and is a SEMANTIC change (alias/extra handling) -# that needs its own gate — not folded into a no-output-change lane. It is worth that gate: on a cold -# llvm-project map of the FIXED binary (`sample`, 12 s of a 46 s run, 127 453 busy leaf samples), -# `ts_node_child_iterator_next` is still the #1 leaf at 14.26%, and attributing each of its samples to -# the nearest non-tree-sitter caller puts bindsVisitNode SECOND at 5 614 samples — 30% of that leaf's -# whole cost, behind captureTagsFacts' 7 304 (the tags-query pass, a different shape). Then +# * src/ingest_binds.h (bindsVisitNode) IS converted now — arms (A9) and (B7), the gate this lane W2 +# asked for. Its declarator loop needed the INDEX for `ts_node_field_name_for_child( n, i )`, which is +# the same restarting scan as `ts_node_child` and made the loop quadratic THREE times over per node +# (one child fetch, two field lookups). The fix is the cursor's own O(1) +# `ts_tree_cursor_current_field_name`, which is a SEMANTIC substitution, not a mechanical one — hence +# this arm rather than a fold into a no-output-change lane. The two agree exactly, and the vendored +# source says why: `ts_node_field_name_for_child` returns NULL for an EXTRA child and otherwise looks +# up the non-inherited field entry at the child's structural index in its parent production, falling +# back to the nearest field name inherited on the way down through invisible nodes +# (third_party/deps/tree_sitter/lib/src/node.c:689); `ts_tree_cursor_current_field_id` performs the +# identical lookup by walking the cursor's stack UP through exactly those invisible ancestors, breaks +# on an extra, and stops at the next visible one (lib/src/tree_cursor.c:657). Same set, same +# precedence, O(1) instead of O(C). +# Why it was worth it: on a cold llvm-project map of the W2 binary (`sample`, 12 s of a 46 s run, +# 127 453 busy leaf samples) `ts_node_child_iterator_next` was still the #1 leaf at 14.26%, and +# attributing its samples to the nearest non-tree-sitter caller put bindsVisitNode SECOND at 5 614 — +# 30% of that leaf, behind captureTagsFacts' 7 304 (the tags-query pass, a different shape). Then # qualifierOf 2 629, enclosingScopeOf 1 040, cc_isCountableLocalDecl 676, cc_walk 531. +# bindsVisitNode's TypeScript `type_annotation` scan was converted in the same pass: it breaks at the +# first `type_identifier`, but nothing bounds how many comments precede one. # * src/ingest_names.h:61 (firstChildOfType) keeps the indexed form: both callers pass a # `using_declaration` / `qualified_identifier`, whose width comes from the grammar, and a per-call # cursor allocation would cost more than the scan it replaces. Class 3 in practice, not class 2. -# * The ~37 class-3 sites (base clauses, parameter/argument lists, attribute lists, fixed-index probes) -# keep the indexed form on purpose — see the note on src/infra/tschildren.h. +# * THE CLASS-3 TABLE WAS WRONG, AND ARMS (B10..B12) ARE WHAT SHOWS IT. The P1-0 follow-up called ~37 +# sites "class 3 — width from the GRAMMAR, correct as written": base clauses, argument and parameter +# lists, attribute lists, a declaration's declarators. A comment can sit between ANY two children of +# ANY node and tree-sitter splices the extra into that node's own child array, so a list's width is set +# by the FILE wherever a comment may legally appear in it. Measured on the W2 binary, 16 000 comments +# inside ONE list vs the identical flood just outside it: `declaration` 77x (B7), `argument_list` 56x +# (B11), lambda capture list 26x (B12), `base_class_clause` 13x (B10). All four are converted. +# WHAT REMAINS, and the sweep that bounds it. Fifteen language shapes were flooded the same way and +# timed on the FIXED binary — a C++ `using` declaration, a template argument list, a brace initializer, +# a struct field list, an enum body, a parameter list, a Python argument list and base list, a JS object +# literal, a TS type annotation, a Ruby class body, a Go call, a Java implements list, a Rust call, and +# a 16 000-paragraph markdown document (mdWalk, whose root IS the file). Every one came out at 0.01-0.10 s, +# i.e. flat, because the balanced `_repeat` subtree absorbs the flood at those sites or the indexed loop +# is not on the map path. That is EVIDENCE OF ABSENCE FOR THOSE FIFTEEN SHAPES ONLY — it is not a proof +# that the ~25 remaining indexed loops are safe (src/ingest_relations.h, ingest_names.h, ingest_docs.h, +# ingest_elixir.h, ingest_sidecap.h, pattern.h). Each still needs the one-line reason the note on +# src/infra/tschildren.h now demands, or a cursor. Handed over whole rather than half-converted. # # Exit 0 = ALL PASS, non-zero = SOME FAILED. @@ -146,6 +171,45 @@ for n in ( 1000, 16000 ): + [ " x = loc__0;", " }" ] + [ C ] * n + [ " return x;", "}" ] ) # findMatches (root flood) + matchChildren (the candidate compound_statement's own flood) w( "pat/n%d/big.c" % n, [ "void f( void )", "{", " int a;" ] + [ C ] * n + [ "}" ] + [ C ] * n ) + # bindsVisitNode — N comments as children of ONE `declaration` node (they sit between the type and + # the declarator, so the declaration's own child list is the flood). TWO same-named methods make the + # declarator binding OBSERVABLE: with it, `a.m()` resolves to Foo::m alone; without it the call is + # ambiguous and BOTH methods get a caller (measured, arm A9). + w( "binds/n%d/big.cpp" % n, + [ "struct Foo { int m( void ); };", "struct Bar { int m( void ); };", "int bigfun( void )", "{", " Foo" ] + + [ C ] * n + [ " a;", " return a.m();", "}" ] ) + # …and its control: the identical flood in the same body, OUTSIDE the declaration (the walk is still + # entered on the declaration — its child list is just 3 wide instead of N) + w( "binds_off/n%d/big.cpp" % n, + [ "struct Foo { int m( void ); };", "struct Bar { int m( void ); };", "int bigfun( void )", "{", " Foo a;" ] + + [ C ] * n + [ " return a.m();", "}" ] ) + # captureBases — N comments as children of ONE `base_class_clause`. This list LOOKS grammar-bounded (a + # class's base types) and the P1-0 follow-up table called it class 3 for that reason; EXTRAS refute it. + w( "bases/n%d/big.cpp" % n, + [ "struct A { int m( void ); };", "struct B : public A," ] + [ C ] * n + + [ " public A { int q( void ); };" ] ) + # …and its control: the identical flood between the two structs, outside any clause + w( "bases_off/n%d/big.cpp" % n, + [ "struct A { int m( void ); };" ] + [ C ] * n + + [ "struct B : public A, public A { int q( void ); };" ] ) + # ccCallArity's argument scan — N comments inside ONE `argument_list`. Same refutation: the scan already + # skipped `comment` children by kind, which is the author knowing they land here, indexed anyway. + w( "args/n%d/big.c" % n, + [ "int g( int a, int b );", "int f( void )", "{", " return g( 1," ] + [ C ] * n + [ " 2 );", "}" ] ) + w( "args_off/n%d/big.c" % n, + [ "int g( int a, int b );", "int f( void )", "{", " return g( 1, 2 );" ] + [ C ] * n + [ "}" ] ) + # captureLambdaShadowDecls — N comments inside ONE lambda capture list + w( "lcap/n%d/big.cpp" % n, + [ "int f( int x )", "{", " auto L = [ x," ] + [ C ] * n + [ " & ](){ return x; };", " return L();", "}" ] ) + w( "lcap_off/n%d/big.cpp" % n, + [ "int f( int x )", "{", " auto L = [ x, & ](){ return x; };" ] + [ C ] * n + [ " return L();", "}" ] ) + # SliceRdWalker (--slice's rung-3 flow walk) — N comments on either side of an `if` INSIDE the sliced + # definition's body, so seq/structure/hasStructureBelow/ifC are the wide loops. slicew above floods the + # ROOT and leaves the definition narrow, which is why it never reached this walk. + w( "slicerd/n%d/big.c" % n, + [ "int helper( int x );", "int target( int x )", "{", " int acc = x;" ] + [ C ] * n + + [ " if( x > 0 )", " {", " acc = x + 1;", " }" ] + [ C ] * n + + [ " return helper( acc );", "}" ] ) PY # user-CPU seconds (user+sys) of one cold run of "$@" against corpus $1 @@ -230,6 +294,22 @@ if [ "$( count_rows "$TMP/a_pat.xml" '' )" = 1 ]; then else no "(A7) findMatches/matchChildren: the pattern lost its match on the flooded body" fi +callers_count(){ # $1 = corpus, $2 = fully qualified symbol -> the callers verb's count= attribute + "$BIN" "$1" --no-cache --callers="$2" 2>/dev/null | sed -n 's/.*]*count="\([0-9]*\)".*/\1/p' +} +A_FOO="$( callers_count "$TMP/binds/n1000" 'big.cpp::Foo::m' )" +A_BAR="$( callers_count "$TMP/binds/n1000" 'big.cpp::Bar::m' )" +if [ "$A_FOO" = 1 ] && [ "$A_BAR" = 0 ]; then + ok "(A9) bindsVisitNode: \`Foo a;\` still binds a->Foo past a 1000-comment declaration (Foo::m 1 caller, Bar::m 0)" +else + no "(A9) bindsVisitNode: the declarator binding is gone — expected Foo::m=1 Bar::m=0, got $A_FOO / $A_BAR (ambiguous a.m() gives 1/1)" +fi +"$BIN" "$TMP/slicerd/n1000" --no-cache --slice=target:acc >"$TMP/a_rd.xml" 2>/dev/null +if [ "$( count_rows "$TMP/a_rd.xml" '' )" = 1 ]; then + ok "(A10) SliceRdWalker: the use at line 2009 still joins BOTH defs (rd=\"4,1007\") across a 2000-comment body" +else + no "(A10) SliceRdWalker: the reaching-def join is wrong — expected rd=\"4,1007\" on the line-2009 use row" +fi "$BIN" "$TMP/slicew/n1000" --no-cache --slice=target >"$TMP/a_slice2.xml" 2>/dev/null if [ ! -s "$TMP/a_slice.xml" ]; then no "(A8) determinism (empty --slice answer)" @@ -267,6 +347,30 @@ b_pat_map="$( usercpu "$TMP/pat/n16000" "$BIN" --top-k=100000 )" b_pat_walk="$( usercpu "$TMP/pat/n16000" "$BIN" --pattern='{ int a; ... }' )" arm "(B6) findMatches/matchChildren" "$b_pat_map" "$b_pat_walk" 8 0.30 "--pattern over a 16000-comment root and body vs the plain map" +b_binds_off="$( usercpu "$TMP/binds_off/n16000" "$BIN" --top-k=100000 )" +b_binds_on="$( usercpu "$TMP/binds/n16000" "$BIN" --top-k=100000 )" +arm "(B7) bindsVisitNode" "$b_binds_off" "$b_binds_on" 8 0.30 "one declaration 16000 children wide vs the identical flood beside it in the same body" + +b_srd_map="$( usercpu "$TMP/slicerd/n16000" "$BIN" --top-k=100000 )" +b_srd_walk="$( usercpu "$TMP/slicerd/n16000" "$BIN" --slice=target )" +arm "(B8) SliceRdWalker" "$b_srd_map" "$b_srd_walk" 8 0.30 "--slice's flow walk over a 16000-comment definition body vs the plain map of the same file" + +b_spp_map="$( usercpu "$TMP/slicepp/n16000" "$BIN" --top-k=100000 )" +b_spp_walk="$( usercpu "$TMP/slicepp/n16000" "$BIN" --slice=target )" +arm "(B9) sliceWalkPreproc/preprocC" "$b_spp_map" "$b_spp_walk" 8 0.30 "--slice over a 16000-comment \`#if\` block inside the definition vs the plain map" + +b_bases_off="$( usercpu "$TMP/bases_off/n16000" "$BIN" --top-k=100000 )" +b_bases_on="$( usercpu "$TMP/bases/n16000" "$BIN" --top-k=100000 )" +arm "(B10) captureBases" "$b_bases_off" "$b_bases_on" 8 0.30 "a base_class_clause 16000 children wide vs the identical flood outside any clause" + +b_args_off="$( usercpu "$TMP/args_off/n16000" "$BIN" --top-k=100000 )" +b_args_on="$( usercpu "$TMP/args/n16000" "$BIN" --top-k=100000 )" +arm "(B11) ccCallArity" "$b_args_off" "$b_args_on" 8 0.30 "an argument_list 16000 children wide vs the identical flood outside the call" + +b_lcap_off="$( usercpu "$TMP/lcap_off/n16000" "$BIN" --top-k=100000 )" +b_lcap_on="$( usercpu "$TMP/lcap/n16000" "$BIN" --top-k=100000 )" +arm "(B12) captureLambdaShadowDecls" "$b_lcap_off" "$b_lcap_on" 8 0.30 "a lambda capture list 16000 children wide vs the identical flood outside it" + # ── (C) byte-identical against a reference binary ──────────────────────────────────────────────────── echo echo "=== (C) byte-identical output vs RIPWIRE_REF_BIN ===" @@ -289,7 +393,8 @@ else fi } for n in n1000 n16000; do - for d in slicew slicepp span health health_off ffi ffi_off locals pat; do + for d in slicew slicepp span health health_off ffi ffi_off locals pat binds binds_off slicerd \ + bases bases_off args args_off lcap lcap_off; do cmp_pair "$TMP/$d/$n" --top-k=100000 done cmp_pair "$TMP/slicew/$n" --slice=target @@ -298,6 +403,13 @@ else cmp_pair "$TMP/health/$n" --grep=pad cmp_pair "$TMP/locals/$n" --lint --naming-locals cmp_pair "$TMP/pat/$n" --pattern='{ int a; ... }' + cmp_pair "$TMP/binds/$n" --callers='big.cpp::Foo::m' + cmp_pair "$TMP/binds/$n" --callers='big.cpp::Bar::m' + cmp_pair "$TMP/binds_off/$n" --callers='big.cpp::Foo::m' + cmp_pair "$TMP/slicerd/$n" --slice=target + cmp_pair "$TMP/slicerd/$n" --slice=target:acc + cmp_pair "$TMP/slicerd/$n" --slice=target:acc --slice-flow=both + cmp_pair "$TMP/slicepp/$n" --slice=target:acc --slice-flow=both done [ "$c_fail" = 0 ] && ok "(C1) $c_seen generated fixture x verb pairs are byte-identical to the reference" c_fail=0 @@ -326,6 +438,14 @@ case "$( verdict 0.02 2.43 8 0.30 )" in quad\ *) ok "(D) the measured pre-change findMatches pair (0.02s vs 2.43s) IS called quad";; *) no "(D) the isolation verdict cannot see the largest pathology it was written against";; esac +case "$( verdict 0.11 7.71 8 0.30 )" in + quad\ *) ok "(D) the measured pre-change bindsVisitNode pair (0.11s vs 7.71s) IS called quad";; + *) no "(D) the isolation verdict cannot see the bindsVisitNode pathology";; +esac +case "$( verdict 0.03 2.52 8 0.30 )" in + quad\ *) ok "(D) the measured pre-change SliceRdWalker pair (0.03s vs 2.52s) IS called quad";; + *) no "(D) the isolation verdict cannot see the SliceRdWalker pathology";; +esac case "$( verdict 0.10 0.70 8 0.30 )" in linear\ *) ok "(D) a 7x pair (0.10s vs 0.70s) IS called linear, not quad";; *) no "(D) the isolation verdict calls a linear pair quadratic";; From edcea4b77b25c2143a052105ad0ed78a92badec9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:00:28 -0400 Subject: [PATCH 43/73] =?UTF-8?q?chore(integ):=20regenerate=20docs/TUNING.?= =?UTF-8?q?md=20from=20the=20sweep=20records=20on=20the=20merged=20tree;?= =?UTF-8?q?=20ack=20ledger=20healed=20through=20the=20binary=20(1,249=20?= =?UTF-8?q?=E2=86=92=201,233=20rows,=20duplicate=20halves=20collapsed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .ripwire_quality_acks | 138 +++++++++++++++++++----------------------- docs/TUNING.md | 10 +-- 2 files changed, 66 insertions(+), 82 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 19b4aa511..68d08668e 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -8,6 +8,7 @@ ack api-surface 1039e3c8e0fc3667 4 cid=4efcfe9cb7f739a2 M12 (capture-audit L9): ack api-surface 105c48e20c80c896 3 cid=720fab31ea99ebde A2: unmeasuredHintNote gained the AbsHintFrame parameter one commit after this lane introduced it (4db6fb3). It is a header-inline helper in namespace mcpedit with exactly one caller, resolveOneForEdit, in the same file; no consumer outside this lane ever saw the 2-arg form. The widening is what makes the never-parsed disclosure and the symbol scan agree about which files a hint names -- two copies of that rule is the defect this replaces. ack api-surface 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept ack api-surface 10f47dd5a3f35d86 5 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history +ack api-surface 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 15754e3561a34f40 7 cid=e3721579f68947f6 deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack api-surface 163c0a0eb3219fa9 5 cid=9e7d5dab8c14a887 R2: prEmptyRootTail gains the truncated= parameter it needs to carry budget-floor-exceeded — deliberate, 1 caller, incompatible=0 (--edit-check contract-change); prEmptyRootPrice is the new file-scope helper that decides the label and re-prices, keeping writePrContext's own complexity and LOC unchanged | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack api-surface 1689c98fa4eac33e 4 cid=f5ec9b69e2526e08 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -34,6 +35,7 @@ ack api-surface 2e6026bd58111ad5 5 cid=3d6b010d178e2c8f by=src/* lane/n6-d, the ack api-surface 30dfe3580e3235a8 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 32e780668c108fa5 5 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack api-surface 33d55f3b93bc79ea 4 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack api-surface 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 3561d0281d324276 14 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 3703c22e2f2112bd 5 cid=e07b67ecf3068d1d by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack api-surface 3877dd1e9b4ae997 4 R-E (2026-08-17 harvest): narrowLegoToRenderedSigs needs an explicit rootPrefix param because packSignatures' sigsRendered rows are already root-relative while the function's own escaped path comparison was still absolute -- every comparison silently failed, narrowing legoScoped to nothing on every --for run whose rendered sigs hit this path. The +1 param (defaulted, so every other caller is unaffected) and the small cx/LOC bump on narrowLegoToRenderedSigs and its one caller runForLens are the minimal fix; caught by legobundlecheck.sh going red for the wrong reason. churn=self is this lane's own edit window. @@ -41,6 +43,7 @@ ack api-surface 39588f57bd7b46b5 6 cid=2116294439c0a50f M13 paging/budget parity ack api-surface 3a54ea98a485670c 2 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 api-surface 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack api-surface 3cb849a8c8a4aa2d 5 cid=fce85f6f2f55e1af lane V1 N2 (f5913f3): grepTierAttrs/grepTierKeys gain floorAlreadyEmitted, resolveCandidates gains capFired — one explicit parameter each, every caller updated in the same commit +ack api-surface 3d87404c1cdf50ec 3 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 3da8557837c77591 7 cid=d9d62f11f9760dc1 by=src/* §N6-C .gitignore-by-default: the crawl gains an ignore mode. The two api-surface/params rows are ONE deliberate contract change — ingest()/collectSources() take a trailing defaulted respectGitignore, the only way a CLI flag can reach the crawl without a global; the three short-horizon-churn rows are this lane's own edits to the flag ledger, the crawl and the --skipped verb, which is what adding a flag with a disclosure IS; collectSources +3 ccx / +11 LOC is what remains after the probe, the mode and the prune fan-out were extracted into probeIgnoreSet/recordDirPrune (it was +15/+43 inline). ack api-surface 3eaf4cfffc6ac8b9 3 cid=7e40913eaabff86f rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack api-surface 43e576a9c6592de5 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -49,6 +52,7 @@ ack api-surface 44b41056b999b501 6 R-R root-relative emission lane: threading th ack api-surface 44b4c7b78ee480d6 4 cid=6da24121cdbb52f4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface 44bac7b0ded56eb7 3 one grouped astQuery walk for --lint three built-in packs: cacheFriendliness takes its captures as a param (+1, deliberate contract) because --lint is its only caller and its own corpus-wide read+parse+compile pass was pure duplicate work; churn=self on astQuery/mergeAtomsPack/mergeCachePack/runLint is this one change own edit window. Output byte-identical on a frozen C++ corpus (1075 files) and a pure-Python corpus; 14 lint-family gates green; warm --lint 1.34s to 0.42s ack api-surface 451442e5d1031096 5 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack api-surface 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 49ea9954a4e77a06 6 cid=cf442ef6cda47ce0 P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). ack api-surface 4b788e6f0a75bc80 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 4b8467d6559779e4 3 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history @@ -64,9 +68,11 @@ ack api-surface 574641dcc1bdf0ec 13 WAVE-2 close (2026-08-19), finding 3 of 3: t ack api-surface 5774f0f445361430 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 59f050855874875f 6 cid=79ec141006215e25 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 5a07390012b46e06 9 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack api-surface 5c2c4a2b311b8dba 4 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 5e5cc30bcbc1fb63 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 5ec38fbd414fa4d4 7 cid=6f20993f1b475898 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 61e5df9e1e40ff70 5 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 api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 654551bf984cc299 5 cid=913136f787574436 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 6e58b0a307757079 24 cid=cb5c8aaa7451a632 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack api-surface 75720b711509b5b9 5 cid=a54cd0ffa32a9952 lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked @@ -86,10 +92,12 @@ ack api-surface 84b6bfc164c989e8 2 cid=87c39ba5f968fb34 M21(a) sa sym=/p=: stale ack api-surface 851e83b4505f10f6 12 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 85dd951a8f730eec 4 cid=4e0fbe2c7a019909 P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface 861092ef53c6c2dd 4 cid=48c86b50ca49ddb4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) +ack api-surface 86db2ff4e22cae54 6 cid=b9c9829a2aac834e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 8a92173ded649e17 14 cid=f76e97c84ac7c168 by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack api-surface 8d58de9bb922f582 4 T1 completeness claims (complete= on grep/whereis): the +1 on streamBlobs is the deliberate DEFAULTED StreamBlobStats* param (null-object sink inside, no per-site null test; every existing caller byte-identical) so whereis can prove its scan exhaustive before claiming; cx/LOC on streamBlobs/computeWhereis/writeWhereisPage/emitGrepReport is the claim computation plus its in-band legend (the honesty text IS the feature); churn=self on those plus grepCollect/dispatchMcpLine is this lane own edit window. Gated red-first by test/completecheck.sh (24 arms, 10 red pre-fix; mutation arms force cap/offset/budget/unreadable-file/regex-mode/oversized-blob and assert the attribute VANISHES); full plain suite green, 21 touched-family gates green under ASan+LSan, determinism x3, xmllint clean ack api-surface 925094be92085dae 3 cid=714cea1e1b31a1ae A6: rollbackMessage gained a 'cause' parameter one commit after this lane introduced it (57fe5fc). It is a header-inline helper in namespace rw::editplan with two callers, both in the same function in the same file; no consumer outside this lane ever saw the 2-arg form. The parameter is what lets the concurrent-write abort reuse the rollback disposition wording instead of growing a second copy of it. ack api-surface 92ac9caf38b8aab0 4 root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh +ack api-surface 95cc88ca4aab7039 4 cid=25c411d4871bda46 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 96fdcdff2f0ff0f7 4 R-H span tiers (2026-08-19 wave-3 lane, harvest R-H / experiment E5). The nine gating rows are ONE change, read line by line before acking. (1) api-surface grepHitsJson 3->4 params + verbosity: the MCP grep verb takes the span-tier MODE, because the escape hatch has to exist on the MCP surface too — an MCP-only agent that reads suppressed_comment= has no CLI to re-ask from; deliberate contract-change. WAVE-3 VERIFIER CORRECTION (P6-1): this reason originally read 'both callers updated in the same commit' and that was FALSE - src/mcpverbs.h's batch arm still took the defaulted GrepIn::Code and read no 'in' field at all, so the hatch was closed on the ONE surface that had no CLI fallback. Closed in the wave-3 fix lane: both callers now read the value through the same closed-value reader (mcpverbs.h::grepInModeFromArg), 'in' is a declared kBatchSubQueryFields member, and greptiercheck arms (9b)/(9c) pin the batch hatch and its refusal. (2) parseArgs +6 cx / +14 LOC and dispatchMcpLine +3 cx: one new closed-value flag arm (--grep-in=code|any) and its MCP twin, the same shape --grep-scope= added; a flag cannot be added to a hand-rolled parser without them. (3) churn=self on emitGrepReport / grepHitsJson / measure_set: this change's own edit window, not a history signal. (4) emitGrepReport +20 LOC / grepHitsJson +14 LOC: the filter call plus its wiring — the six conditional appends and the legend clause were already lifted into grepTierAttrs/grepTierLegend/grepTierKeys (the grepUnindexedAttrs/grepUnindexedKeys pattern), which is why the COMPLEXITY regressions on both are gone. Nothing here is a shortcut: the tier policy lives in search.h::grepApplySpanTiers and the parse in ingest.cpp::spanTiersOfFiles, both new symbols with their own gate (test/greptiercheck.sh - 30 arms at the wave-3 fix-lane head, 18 FAIL on the clean adb0831 pre-lane binary, 0 here; this text read '22 arms, 12 red', written against an earlier revision of the gate and never refreshed - WAVE-3 VERIFIER CORRECTION P6-7, and an ack's reason is the artifact a future reader trusts instead of re-deriving). ack api-surface 983814f2b5912a90 3 cid=fe2cfa34166f503a M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface 9ebbafddddd086b4 7 cid=50e8788010378ebe capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -98,6 +106,7 @@ ack api-surface a783c75feea9f253 3 cid=7345e4b8de2407c9 2026-09-06 stranger-audi ack api-surface a8879e4655677c7d 4 cid=cb0c898f2646cc43 P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface a8b774025a21bdc6 6 cid=8c396521251254f3 M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. ack api-surface a8cd12b856dd8307 2 cid=666f9172fa28bbb0 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 api-surface ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface ad50354348e0b35e 7 cid=1ca210dbe9314b90 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface aeed75863f7b617d 3 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface b496a273ae1564ef 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. @@ -106,6 +115,7 @@ ack api-surface b689422adfc04435 5 cid=cdb3a6ea16c83186 lift-disclosure round (2 ack api-surface bb2c0b847815a0ca 4 cid=9eb5f97927595a07 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1: the MCP edit_check verb mirrors the CLI pre-apply preview through the SAME editpreview::run, so the two surfaces cannot answer differently (gate arm N pins them document-for-document). new_body is optional and defaulted; every existing call site is untouched and the verb stays readOnlyHint true — passing it previews, it never writes. ack api-surface bcb3377087f2e034 7 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface be8176514288abc7 2 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack api-surface c1e1d72154d6784f 5 cid=c495b60a16e1ce45 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface c22ce1db6b79b087 4 cid=3b28778477fcc103 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface c30037c3e4f345a9 3 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack api-surface c4d50b7393d16274 6 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. @@ -116,11 +126,15 @@ ack api-surface cb0f0b806aa1e4e8 5 WAVE-2 close (2026-08-19), finding 3 of 3: th ack api-surface cb7342964b38db9c 3 lane/r10-cheap-buckets (r10 GitNexus fix round, LB-A + LB-G). SIX gating rows, ONE lane, read one by one before acking. THREE api-surface contract-changes, all deliberate parameter additions that ARE the feature: (a) mcpverbs usesText 2->3 params, taking McpPageArgs exactly as impactText already did, because the MCP uses verb gained the same default site cap as the CLI and an MCP-only agent that reads capped=1 needs a hatch it can reach (mcpclidiffcheck LENS 1 pins the two surfaces' root-attribute sets equal, so capping one and not the other is a divergence, not a saving); both dispatch sites AND kMcpVerbFields updated in the SAME commit, verified by mcpclidiffcheck/mcpverbscheck/usescheck green. (b)+(c) serialize packSignatures 17->18 and packSignaturesJson 11->12, both taking a trailing hasRelevanceFloor bool, default false so every non---for caller is byte-identical (verified: default map, pack-task, expand, exemplar, recall, hotspots, callers, grep, impact all unchanged). The flag cannot be replaced by passing a smaller topN, because those emitters read topN==0 as ALL, so a query nothing scores on would emit the whole corpus. THREE verbosity rows are the new code itself, already cut twice in this lane: duplicated rule bodies hoisted into relevanceFloorCut/pathTierIndexOver/compareTierThenPath, then the restated rationale moved to those helpers' headers - together taking gating from 13 to 6. What remains is runForLens +10, runCallHierarchy +11 and usesText +14 lines of genuinely new behaviour (the floor cut and its note plumbing; the tier index, the page window and the conditional legend clause). Splitting runForLens is a real refactor of its own - it was 658 lines before this lane touched it - and does not belong in an output-composition fix round. ack api-surface d1e50dc6d815cf27 6 cid=574545e642c44ece lane B1 cap disclosure: these five out-params ARE the disclosure. extractMentions, liftPackageDirMention, gitLogFileSets, gitRecentCommitFileSets and applyCoChangeBoost each gain ONE census output so a cap that cut invisible content can be told apart from a corpus that simply ran out, and none of the five facts is reconstructable downstream — the caller cannot see what the indexer refused to index. Every one is defaulted or updated at every call site in the same commit. ack api-surface d3b5cec59d7a7684 5 cid=5b456bf45dd0d9c9 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) +ack api-surface d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack api-surface 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 api-surface db9af56c7a3f5bee 4 cid=bec957a0aa99147d P7 (terminality round A, lane R): appendOneNote / renderNoteChildren / appendJsonNoteArray grow ONE trailing defaulted parameter (the file-note target p= / the JSON key) so a FILE note can ride a row now that the wrapper is gone (rank-ordered flat ); every existing caller compiles unchanged — --edit-check: contract-change, incompatible=0 on all three; gate test/forrankordercheck.sh arm 4 ack api-surface dd2f935e1818171a 5 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack api-surface ddc3e2a475f782a1 4 cid=4333f578e348e95f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept ack api-surface e514a69013d0934c 6 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B +ack api-surface e57abf94a3b1f3a4 5 cid=788aa47a3ec3d3f2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface e6bdc5187566f3d7 8 cid=615b35e39fe420e4 capture-audit 2026-09-04 wave-1 close: deliberate contract changes, each --edit-check incompatible=0 in its lane report — L4 (lane-L4.md, Symbols whose contract changed): pageDisclosure/computePageDisclosure +collectionCapped (defaulted), packLego +graphCountFloorAttr (defaulted), packConnect/writePrRootOpen/writeTestGateReport/writeTestGateReportJson/memberUsesArm +const Graph& for the M15 gauge; L5: enumerateRefs +filterNameHits out-param (defaulted, three callers source-compatible); L3: emitGrepUnindexed/grepAuxJson +window (the H4 paging fix, caller updated) ack api-surface ed05ee0357d016f1 4 --lint reads the corpus ONCE (audit lane B2, second half): astQueryGrouped gains an OPT-IN keptBytesOut, so the walk that already reads every file hands its bytes to the two symbol-level passes that ran after it instead of each re-opening the same ~900 files one at a time on the main thread. The +1 param on astQueryGrouped/namingLensChecks/appendNamingFindings is that deliberate contract and it is DEFAULTED — the --ensemble caller passes nothing and is byte-identical, verified against a pre-binary. Partial by construction and safe by construction: an empty slot (skipped file, or a genuinely empty one) falls through to the caller's own read, which returns the same bytes, so fast and slow paths cannot disagree; a size guard keeps a vector built for another corpus in bounds. Retention is a SINGLE point placed before the tree is built, so no exit can forget it and no branch can keep it twice; workers only ever write distinct pre-sized slots, verified under ASan+UBSan (-fno-sanitize-recover=all, LSan suppressions) clean on both corpora with output still identical. Cost measured honestly: peak RSS 182.5 -> 192.7 MB (+5.6%) for one corpus of text held across the lint block. churn=self on astQueryGrouped/builtInLintCaptures/runLint/lintSymbolLevelChecks/mergeNamingLens is lane B (one day earlier) plus this round own first commit. Frozen-corpus profile: mergeNamingLens 19.5 -> 6.3 ms (the naminglens getBytes row is GONE, 907 calls -> 0), lintSymbolLevelChecks 45.1 -> 37.7 ms, readFile 2335 -> 1168 calls; warm --lint 0.51-0.52s -> 0.42-0.43s. 20 gates green, determinism + xmllint clean ack api-surface eea83c3db0f03d69 20 cid=a4f7862584788fbd by=src/* lane 2 of the Graft head-to-head (2026-09-07): packSignatures and packSignaturesJson each gain ONE defaulted trailing out-parameter, the ids of the sigs rows they actually emitted, so the file-grain tail can exclude those files instead of the whole 40-candidate surface (three single-file answers at candidate rank 5/10/5 were served nowhere on rocksdb). Every existing caller is byte-identical; the facet is the deliberate arity change the ack-only help names. | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. @@ -171,6 +185,7 @@ ack api-surface:new-symbol 222f47325b7191d9 0 cid=883aa0365c34d51e timsort vendo ack api-surface:new-symbol 246cc6ad10ff06bf 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack api-surface:new-symbol 25c9f0504fd376a4 0 cid=61a6b84838b35798 R1 lane V2, remainder: four api-surface new-symbol rows are the deliberate extraction this fix chose over inline growth — rw::kOverCeilingLegend (ONE wording for three surfaces, hoisted out of a function-local constant the MCP twin could not reach), rw::priceForTaskRoot (the MCP for root's price-and-label step, lifted whole out of a 337-line body), forLensJsonBudgetStanza and forLensJsonOverCeiling (the forLensNotesStanza/forLensJsonTailStanza precedent in the same file). None widens a shipped CLI or MCP contract: no new flag, no new verb, no changed signature. verbosity runForLens 990 to 993 is three comment lines recording where the over_ceiling legend wording now lives. ack api-surface:new-symbol 26aa7c55d54c4341 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean +ack api-surface:new-symbol 26e3f9a6bff9ea57 0 cid=942faaac6ddd5389 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 27e7bddec77b7abb 0 cid=52e76f37716b687a lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack api-surface:new-symbol 2978aaf0cb86e41e 0 cid=54ff85631f901cea or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack api-surface:new-symbol 2af9a541302cfd51 0 cid=a8ea687c59e866e3 by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. @@ -221,6 +236,7 @@ ack api-surface:new-symbol 62fc004629681a50 0 cid=bb30f060c75deab1 M13 paging/bu ack api-surface:new-symbol 6360fe1b523a5602 0 module-constant round (2026-08-12, test/moduleconstcheck.sh): the four short-horizon-churn rows are the documented extraction-bump protocol itself — kParserVer and its quality.h mirror MUST move in the same diff (qextractionkeycheck), dropConstantCapture is the policy function this round exists to change, and cudaMemorySpaceQualifierOf's edit is the dedup the quality gate itself demanded (169-token clone dissolved into childTokenAmong). The 24-token ncBoolTypeName|cudaMemorySpaceQualifierOf pair is a cross-domain wrapper-shape coincidence (naming-lens vocab membership vs tree-sitter child scan over disjoint token sets in different files); merging them would be the wrong abstraction the delta header warns against. ack api-surface:new-symbol 63e57d342c401094 0 cid=f31eb03cde31c7a4 E1 (terminality round A, lane E): the seam rules' disclosure record (trailingNewlineFolded, separatorPadded) ack api-surface:new-symbol 64a91ab838dd6c45 0 cid=c371f3609107eee5 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions +ack api-surface:new-symbol 659af2613fd4ba75 0 cid=a69e85d30342478d C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 6659666bc5b97fd1 0 cid=b90dac55f3b271a0 main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical ack api-surface:new-symbol 68442823ab40d356 0 cid=86fc9a0af44d3bf2 M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface:new-symbol 69a0fdb34357274c 0 cid=2b6f842c43e01e62 cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. @@ -232,12 +248,15 @@ ack api-surface:new-symbol 6cd1fd58a259ee2a 0 cid=ccf5a904f4638db2 M1: a 36-toke ack api-surface:new-symbol 6f27216622124253 0 V1 harvest 2026-08-15: withFileContext is an opt-in trailing param (default false) for the sibs=/inc= feature -- additive, backward compatible, every existing caller unaffected ack api-surface:new-symbol 6f6bb3063be1e4c6 0 cid=77b0be7127b0858f E1 (terminality round A, lane E): the terminator a seam spells (CRLF vs LF), so padding matches the file ack api-surface:new-symbol 7244cb7d8ff2b89f 0 cid=54a576dd82024bc7 wave-3 close, H7 hosts (substrfiltercheck --plan/--abi arms): printStrayFilterNoMatch is the ONE sentence for a --stray-content filter that selects no ref, spoken by the bare verb and its two hosts; it shares refuseFlagValue's fprintf shape (33 tokens) but not its contract — arm C pins the 'not a measurement' clause the value-domain sentence lacks +ack api-surface:new-symbol 73690330b833c9fa 0 cid=e5b6815c0a360d29 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7477bca827a815da 0 cid=56abaf5da4a1419d 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 api-surface:new-symbol 74bb913f25167779 0 cid=b28f2989408cb8a5 V1/R2+N4: the five helpers this fix names at file scope (prLegendText, prRootOpenText, prAnchorNoteText, prPriceDocument + PrPriceCtx, kPrCloseTag) are the emitter's own bytes made measurable — every one returns what used to be fprintf'd so est_tokens can price it; writePrContext 371->376 LOC is the net of that extraction (it lost the 21-line legend and gained the price context). ack api-surface:new-symbol 7595f3b659793aee 0 cid=ff0af690126b5e0e 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 api-surface:new-symbol 76cea06ffde88328 0 cid=ceae430130676ebb idiom-class clone: a NAMED one-line std::find predicate. What matches is the std::find idiom itself — 36 normalized tokens, zero shared domain identifiers with ncAnyOf/namesNode, different value types, different subsystems. Inlining it at its two call sites to dodge the row would be metric-gaming; the name is the documentation. +ack api-surface:new-symbol 7723ed789a1c9c09 0 cid=7886e44cd18fba0b C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7857b2ae91f6f363 0 cid=87b7efc7bdc2ece7 F4 (lane F): three new symbols in src/prcontext.h — prEstTokens (ONE estimator for every pr-context root, replacing the ladder's inline formula so the empty root cannot price differently), kPrEmptyDiffBody and prEmptyRootTail. Extracted deliberately: composing the empty root's budget tail inline grew writePrContext (already ccx 154 / 371 LOC) by 2 complexity and 12 LOC; as helpers it grows by neither. No new CLI or MCP surface. ack api-surface:new-symbol 79625906f9f71ad0 0 cid=eee9afc7f54a3a7f E1 (terminality round A, lane E): the ONE spelling of the redaction marker the write surfaces refuse +ack api-surface:new-symbol 79cf899b082e5c8d 0 cid=99549639c3506907 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 7a04eee0ff6ec2d7 0 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack api-surface:new-symbol 7abc8c5ec9dd63a0 0 cid=f43fc221f7b8e8f6 E2 (terminality round A, lane E): SHA-1 modular add in uint64_t and masked: -fsanitize=integer flags an unsigned 32-bit wrap (found by the asan tree) ack api-surface:new-symbol 7bbb17371a4af833 0 cid=d834ff81483d34ad 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. @@ -252,6 +271,7 @@ ack api-surface:new-symbol 82dae23ff5754ef2 0 cid=d72d8662acd735e0 M1: three nam ack api-surface:new-symbol 839f231a03346895 0 cid=ccd63911f3efb68f M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. ack api-surface:new-symbol 8483c65facce2008 0 cid=b6d7989b6d74800b R1: four new file-scope helpers in the mcpedit namespace (kRedactionEllipsis, countRedactionMarkers, redactionMarkerRefusal, redactionMarkerRefusalFor) replace the two constants the substring scan used; they are the shared predicate the three write surfaces call, not a widened public contract ack api-surface:new-symbol 8565479e9b57fb76 0 cid=06bbaf2bb0c34f1a by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. +ack api-surface:new-symbol 882ef9b9b64fd2c1 0 cid=62a45d4a835024bc C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol 8832b6f56a77fdcc 0 cid=27c019875ae26b94 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 api-surface:new-symbol 8ecb190954dce18a 0 cid=ba2fa2ff81d48d5b 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 api-surface:new-symbol 8f70082cef086db9 0 cid=1e87f5e854727c5e 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 @@ -266,6 +286,7 @@ ack api-surface:new-symbol 99ae345ce0cf2182 0 cid=9afdf4c38e8ded3d lane/tc-slice ack api-surface:new-symbol 99e507e0c233d21e 0 cid=9948b608a4486528 E2 (terminality round A, lane E): SHA-1 primitive (git identity, not security) — zero dependencies ack api-surface:new-symbol 9cd214c2e0f6166d 0 cid=027e9c437962f1e2 E2 (terminality round A, lane E): the region budget (2048 B) — over it head/tail/elided_lines, capped:true ack api-surface:new-symbol a04db81aba61fefa 0 cid=e425b3c91591ce8c or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS +ack api-surface:new-symbol a10ea8d3bdca1dfb 0 cid=a64af151dfacd435 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol a32e1579d490db64 0 cid=d11b5bd0646c341f E3 (terminality round A, lane E): the overwrite child's budget (4096 B) — over it head/shown/capped=1/elided_lines ack api-surface:new-symbol a45fd373062fc100 0 cid=d5b6e91fa1f1eb6d by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack api-surface:new-symbol a4a8c133e40fe0ac 0 cid=0dfc15f8d6edef3d M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. @@ -284,6 +305,8 @@ ack api-surface:new-symbol b7ef640cb4f56805 0 cid=37998fcb7c29fd47 M13 paging/bu ack api-surface:new-symbol b996fd75c7b88176 0 cid=f1d29a397ab28c86 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 api-surface:new-symbol c02fa9205afe3baf 0 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 api-surface:new-symbol c0e0ed1f0514d6fe 0 cid=90c371f88c914eac P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. +ack api-surface:new-symbol c26a94415f28768f 0 cid=a0746b8e350afbd2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack api-surface:new-symbol c378ae8278c2ec12 0 cid=d22db6e5cdfe9bfe C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface:new-symbol c4398806f7cd6b15 0 cid=8d80db4bdc937a7f by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. ack api-surface:new-symbol c705c286654dc569 0 cid=4323d40ad174ff5a cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. ack api-surface:new-symbol c7719202f0554d70 0 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. @@ -423,12 +446,13 @@ ack complexity cb7342964b38db9c 33 cid=3af54969638510bc by=src/* member-variable ack complexity d03215f48bec3886 17 cid=d622ffa6d33014f4 E1 (terminality round A, lane E): +1 branch: the redaction-marker refusal beside the NUL one (one ladder, one vocabulary) ack complexity d295ac7080c14dd5 18 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity d42c85b67bd0956f 52 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement -ack complexity d63db6944aa504a7 511 cid=38668b9f7642e38d F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. | prior: capture-audit 2026-09-04 wave-2 merge, lanes L6 + L8 grew the MCP dispatcher past each other's acked magnitude (each lane's own e3b52d3..lane delta was acked to gating=0): L6 M13 (limit/offset/budget_tokens on the seven paging twins, callhierarchy.h shared with the CLI), H14 (lens= declarations, filter= echo), M5 (the verb:arg string grammar read behind isBatchCliSpec) and P11/§5a-3 (legend:compact on slice); L8 P17 (slice/edit_check batch arms) and P9 (post_check read on the three edit verbs). A dispatch chain is its arms; each arm is gated by its own lane's gate (mcpcontractcheck G, mcpattrparitycheck, batchcheck g/h, receiptpostcheck) +ack complexity d63db6944aa504a7 527 cid=1943300f4961480a 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: F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. ack complexity dbe6ed5269d328a4 130 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 complexity dd02b378ae6b5b75 30 cid=5a0958e5bca4aaf3 L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack complexity dd627540f10bba76 166 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity dda0db55532bd5e1 23 cid=848b85e47e6bd67c R1: +2 ccx on editplan::prepare and +1 on editpreview::run — ONE guarded early-return each (the redaction-marker gate moved to where the replaced bytes are known); both were already over the bar before this change and neither gained a nested branch | prior: E3 (terminality round A, lane E): +1 branch/+8 lines: the preview appends its overwrite child before ack complexity dda343d7d36edaba 77 cid=e8803c763bec269d L10: printLintRuleTallyRow's compiled= param and runLintRules' uncompiled-query mapping loop disambiguate a lint-rules query that failed to compile from one that legitimately found zero matches (see docs/PLAN lane L10, finding 3) — deliberate, backward-compatible (defaulted param, incompatible=0) +ack complexity ddc3e2a475f782a1 18 cid=4333f578e348e95f 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 complexity e14feca13c7ae680 57 cid=29dde7810cd9e120 M4 (lane ca-L2): writeHandoffPacket +24 LOC for three emitted facts (run= on , detached=1, candidates=/capped=) and the comments recording why the note match changed; complexity held at 56->57 minor by extracting verifiedNoteTargets() and kHandoffLegend | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck ack complexity e1b4964cea59171b 18 cid=8ba981522099e145 H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. ack complexity e514a69013d0934c 33 cid=aa51679e8cf66000 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B @@ -557,6 +581,7 @@ ack duplication e75742478839369d 31 timsort vendoring: every row is the vendored ack duplication e91c5e004c251a91 24 module-constant round (2026-08-12, test/moduleconstcheck.sh): the four short-horizon-churn rows are the documented extraction-bump protocol itself — kParserVer and its quality.h mirror MUST move in the same diff (qextractionkeycheck), dropConstantCapture is the policy function this round exists to change, and cudaMemorySpaceQualifierOf's edit is the dedup the quality gate itself demanded (169-token clone dissolved into childTokenAmong). The 24-token ncBoolTypeName|cudaMemorySpaceQualifierOf pair is a cross-domain wrapper-shape coincidence (naming-lens vocab membership vs tree-sitter child scan over disjoint token sets in different files); merging them would be the wrong abstraction the delta header warns against. ack duplication e94fce0d5caa811d 66 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 ea4ae03ca699bce4 35 lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). +ack duplication ec3f7b5523723637 110 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack duplication ef3d5272b705b505 107 ingest.cpp split 2026-08-29: pre-split clone pairs (buildNewlineOffsets vs the bench_newline_ab arms, acked lane-B3 at keys 98099b517e9d2fbb/a6f7de52f80c9f48) whose clone-group keys changed because buildNewlineOffsets moved VERBATIM into ingest_astquery.h — the disclosed clone-ack rename floor, same artifact as the main.cpp split's moved-clone row; argvdiffcheck vs c267a4b proves no body changed ack duplication f14cfce02be351ad 24 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication f3518ef93569f6ae 25 idiom-class clone false positive: a three-way ternary over string literals, 25 normalized tokens, sharing no domain identifier with macroRoleAttr and in an unrelated subsystem. Reading both confirms it. @@ -612,11 +637,13 @@ ack params 08d796d1b006560c 6 E1 answer grader + questions task source + claude- ack params 0a3d16d6f3139408 10 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack params 0d7b8741392284d1 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack params 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 19d43d944ddcd186 6 cid=533f2648b2fac227 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 19e15f944de795a8 6 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 3561d0281d324276 13 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. +ack params 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 4b788e6f0a75bc80 6 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack params 4d421276056367a9 6 cid=5a1a2f3caf33a19e timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 5361e2bced6f1988 8 cid=9cf2c52c10b9d878 M12 follow-up (capture-audit L9): --ensemble gained root=/root-relative p= — writeEnsembleReport's 3 new default-valued params (singleRoot/rootPrefix/rootAttr, back-compat) thread the caller's already-computed single-root spelling through; short-horizon-churn on the touched dispatcher. @@ -625,20 +652,24 @@ ack params 5391ffd9aa5765bf 6 cid=8a2aed10e80ed270 P2.2 register-macro dead-code ack params 5710beada2095a34 9 cid=3371683faa811117 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack params 59f050855874875f 6 cid=79ec141006215e25 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack params 5ec38fbd414fa4d4 6 cid=35883c203ba33e28 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 params 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 6e58b0a307757079 23 cid=912117812c5cc12d by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params 775b773b1a3d2349 11 cid=dff500c80ee403ac preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack params 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate ack params 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack params 851e83b4505f10f6 12 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. +ack params 86db2ff4e22cae54 6 cid=b9c9829a2aac834e 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 params 8a92173ded649e17 13 cid=a2c4a7904b16ec5b by=src/* A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack params 901cccac113c5416 8 cid=668f2c9bee810d42 main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical ack params 99ae345ce0cf2182 6 cid=9afdf4c38e8ded3d lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack params 9ebbafddddd086b4 7 cid=50e8788010378ebe capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack params a6718fdf5bbb9f01 6 cid=0a16bab15ec4d994 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params a8b774025a21bdc6 6 cid=8c396521251254f3 M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. +ack params ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params c02fa9205afe3baf 7 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 params ca97a4b6bf07887b 25 cid=72922b04b834af89 by=src/* Phase 4 lane (lpin= disclosure + localityKey tie-break, 2026-09-03): serialize/serializeJson each gain ONE trailing defaulted locPinOut param (the identical shape every honesty counter took — ambOut/unresolvedOut/bind); classifyPin churn=self is the one-line reroute of its Locality outcome through isLocalityPin so the shipped marker and the census label are the same predicate; runAround churn=self is the one-argument extension at its serialize call, the same edit every serialize caller took (main.cpp x4, mcpverbs analyze). Six duplicated sum/at chains folded into counterTotal/counterAt in the same change; astropy map + census byte-identical before and after that fold. ack params cc1e357ae3abf4b0 6 cid=27af8baf185c5ada main.cpp split 2026-08-29, stage 1: deliberate promotion of the six cross-family helpers to their domain headers (gitChangedFiles->situ.h, gitChurnCounts/mineChurnPerFile->gitmine.h, the dead-code trio->quality.h) as rw/rw::quality inline — the only intended api-surface change of the split; bodies verbatim, argvdiffcheck-proven byte-identical +ack params d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params d9c7b2164cb0b8c5 6 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack params dda0db55532bd5e1 12 cid=8515eb8f5b5e8953 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack params e3c54d39d366fbaa 7 cid=4cf915e4faf1fd4a capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept @@ -661,6 +692,7 @@ ack short-horizon-churn 060a064b6ffa7775 44 W1-S2 churn-keying fix (pathQualifie ack short-horizon-churn 0749c4e602daa603 9 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 0777f290bdc69b11 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 07cf5773893d89b0 4 cid=d4a11b38ea395398 Lane V2 item 2 (one ingest per gate): the three rows are short-horizon-churn, churn=self — the fact that GATE_BUDGET_SEC, compactlegendcheck's run() and its rrun() were edited at all. All three edits are the same one-line change (drop --no-cache so the warmed per-root cache is used) plus the two budget rows that change measures; no branch, no symbol and no signature was added, and --quality-delta reports no complexity, verbosity, nesting or duplication movement anywhere in this commit. +ack short-horizon-churn 083de06af84b3a87 30 cid=82bfd94f1c162607 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 0a3d16d6f3139408 13 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: card A1 pre-apply preview: preview= is a DEFAULTED flag on the ONE edit-check assembler rather than a second emitter — two emitters could drift, and a preview that disagrees with the post-hoc answer is worth nothing (test/editpreviewcheck.sh compares the two documents byte-for-byte). The +20 LOC and the churn are the legend sentence that tells a reader the numbers describe bytes that were never written. ack short-horizon-churn 0ab42b0dea75e0c1 4 cid=75fd6d9c66adca24 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) 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 @@ -679,9 +711,10 @@ ack short-horizon-churn 1273fad87a99a9f2 6 cid=e6e7ad831a784a6c M10 (capture-aud ack short-horizon-churn 129d3c8d5a763870 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack short-horizon-churn 131068a6cedf0864 10 cid=bef0a5079a2bf8e4 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit ack short-horizon-churn 15061a69cb5b451f 4 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. +ack short-horizon-churn 1520fa02411735c3 4 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 15754e3561a34f40 39 cid=93c7392c7b677557 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. ack short-horizon-churn 1610c5acaa7d4806 6 cid=545658032a875c30 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn 1624b02e9104560e 151 cid=de44395b061b6b45 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn 1624b02e9104560e 154 cid=85e3075128763497 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn 163c0a0eb3219fa9 10 cid=9e7d5dab8c14a887 R2: short-horizon churn on the three --pr-context symbols this round has been editing (V1 repriced them yesterday, V3 labels them today) — not new debt; writePrContext's complexity and verbosity are unchanged by this commit | prior: V1/R2+N4: --pr-context est_tokens now PRICES the emitted document at 2.50 B/tok. pickPrTrimLevel(2->4) and prEmptyRootTail(3->4) are the deliberate arity changes that carry the price in instead of letting the ladder and the empty root each model one; the three short-horizon-churn rows are this lane's own edits to prcontext.h. ack short-horizon-churn 16e4fa1d32860233 9 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn 1930a35978b9543a 6 cid=b213f6ac9734c995 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. @@ -743,11 +776,13 @@ ack short-horizon-churn 3298be65bf6058ef 100 cid=0d8a43a6b93b2f31 M1: self-churn ack short-horizon-churn 32d067796a0393b8 32 PHP + Lua language port (lane/lang-php-lua, 2026-08-21). All SEVEN remaining gating rows are the SAME class — short-horizon-churn with churn=self, i.e. 'this symbol was edited recently and you edited it again'. That is this change's own edit window, not a history signal, and every one of the seven is a site a language port CANNOT avoid touching: (1) src/model.h::Lang — the enum gains Php(18)/Lua(19); appending is the only safe move (inserting would renumber every on-disk cache key). (2) src/ingest.cpp::kLangTable — the extension->grammar rows for .php/.phtml/.lua, plus the extent 37->40 the compiler enforces. (3) src/main.cpp::computeLangCounts — its two tallies are sized on the LAST enum member, so a new member is a mechanical edit there by construction. (4) src/clones.h::kHashLineCommentLangMask — PHP joins (# IS a PHP line comment), Lua does not (its comment is --, and #t is the length operator). (5) src/lintrules.h::dependencyCapable — PHP true (namespace_use_declaration is captured), Lua false (require is an ordinary call, like Ruby). (6) cc_walk and (7) ev_noteNode — both call isDecisionType/cc_isNestingControl, which now take a Lang so Lua's do...end (a bare scope block, NOT a loop) stops being counted as a decision; every other language is byte-identical. The STRUCTURAL regressions this round did produce were FIXED, not acked: cc_walk +12 cx / +13 LOC from the inline boolean-operator test was extracted to cc_isBooleanJoin, and the duplication that extraction then created against cc_boolOp was removed by giving both ONE shared cc_operatorText. Gates: test/phpcheck.sh + test/luacheck.sh, both shown red (36 and 21 failing arms) against a cd30104-built binary. ack short-horizon-churn 32e780668c108fa5 33 fnbody-require lane: deliberate additive API widening (optional out-params, default nullptr, every existing caller unaffected) to disclose the lazy require/import distinction on --impact's importer tier; residual complexity/duplication is the twin-dialect emitter shape and the dual-mode importersOfFiles scan this feature requires, already extracted where a helper genuinely reduced it (scanImporterEdges, recordLazyPair); short-horizon-churn is this same commit's own edit history ack short-horizon-churn 3385856c74077f1a 22 cid=ef5ec129660c61f5 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. +ack short-horizon-churn 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3561d0281d324276 29 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. ack short-horizon-churn 357ab167dccb9a4d 2 cid=ebe308d04e543d16 by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn 3703c22e2f2112bd 7 cid=e07b67ecf3068d1d by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack short-horizon-churn 3797b511eae7d123 46 cid=52d7844487406324 fix-round follow-on: the recall-ceiling MCP branch (+3 ccx on the dispatcher), the apostrophe word-boundary guards in firstQuotedLiteral (the refusal logic IS the fix), and re-touch churn on the flag table / RecallShape comment / installer timeout re-pin ack short-horizon-churn 37f4883f917f57a8 4 cid=66b110baf5a52e2b wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file +ack short-horizon-churn 380b7de5df1cfd73 6 cid=5c6aaf66598466c8 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 38a5abd610359cfc 20 cid=8cef749bc3ef723e churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. ack short-horizon-churn 38c814a492cc9c8a 4 cid=b804c98a2da4bdec by=src/* A2 (dropped_positive, 2026-09-03): emitForLensJson gained the droppedPositiveStanza, mirroring the existing overCeiling/notesStanza envelope-key shape; self-churn is this round's own fresh edit. ack short-horizon-churn 39680772720129ac 3 cid=7419620b90eb7a8e by=src/* round-4 F-01 (--edit-check false contract-change across files): the ONE gating row is short-horizon-churn churn=self on editCheckContractVsHead, i.e. the footprint of having edited a function this round-3 window already touched — not new debt. The fix itself is a two-line predicate swap: nowDefs is now counted under computeSnapshot own qualityKey behind the same has-a-canonical-id presence gate, so defs_was and defs_now bucket identically and the documented invariant defs_was == defs_now on a clean tree is true rather than merely asserted. Verbosity was NOT acked: the bug explanation moved onto the function doc comment instead, which put editCheckContractVsHead back at its baseline LOC. Gate: test/editcheckcheck.sh arm (j), red-first on all four assertions, Python and C++ two-file fixtures, clean tree AND a real edit in one of the pair. @@ -759,6 +794,7 @@ ack short-horizon-churn 3be1c13661e5a63c 5 pack-task budget round (verifier K1+K ack short-horizon-churn 3c06a3d024349e5f 5 cid=f88a6b24c26cb6b9 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3c07d993bfdbce53 8 cid=3718024f74a80d86 arise-h2h lane 2026-08-31: the four gating rows are all short-horizon-churn churn=self on this lane's own multi-line-statement flow fix (SliceOcc gains stmtLine, sliceWalk anchors it, sliceFlowCompute delegates to the extracted expand helpers, sliceBundleText legend sentence) - the edits are this round's deliberate red-first fix (sliceflowcheck arm 25), no foreign debt absorbed; complexity/nesting/verbosity on sliceFlowCompute were fixed by extraction, not acked | prior: lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 ack short-horizon-churn 3c7a8e2ee4351734 11 fix-grep lane 2026-08-15: this symbol's ONLY change is the boolean line-scope branch swapping the 512-byte-capped DISPLAY helper grepMatchedLine for the new uncapped grepWholeLine. That cap was the bug in both directions (a required --and term past byte 512 dropped a real hit; a forbidden --not term past byte 512 failed to exclude its row), so churn=self here IS the fix and nothing else. Proven by an independent oracle: test/grepandcheck.sh (3a)/(3b)/(3c) now derive truth from /usr/bin/grep over a fixture whose second term sits at ~col 640, and all three arms are RED against the pre-fix binary and green after. Identity restored on this repo: --grep=stale --and=stale 289 -> 292 = plain --grep=stale; --grep=symbol --and=symbol 3028 -> 3030; the five mcp.h sites at cols 513-790 the verifier named are back +ack short-horizon-churn 3d87404c1cdf50ec 88 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 3e0a074094789d60 15 cid=1556a2e06eef44c3 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 3e7c1221865b5b52 13 cid=f636d73c8428270e OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 3e91cee9f02f22a2 12 cid=76f4b09ef77556bd R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. @@ -781,6 +817,7 @@ ack short-horizon-churn 472cc93317130a8b 6 cid=dcdfa60eb93788ed OPTREMARKS F3 (d ack short-horizon-churn 476ab6f670e5d871 13 cid=bb27e8c78edbdb2f OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 48c3932de16d9cdd 9 cid=51e0c2d620ad5d17 by=src/* lane F (F-05/F-06/F-13): any-member scope symmetry for ack suppression + foreign-acks, out-of-scope disclosure now unconditional (never ack-ratcheted), .ripwire_config unrecognized-key/inert-name disclosure — new helpers are the feature surface, short-horizon-churn reflects this round's own edit sequence on these functions ack short-horizon-churn 4975dcbd128411d7 5 cid=9b40c150a9616ae3 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 49e172c2aa455e68 4 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 49e838ca0844258b 5 cid=9d23ed0d32fea19d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 49ea9954a4e77a06 2 cid=ad0e2ee1266299ec selector-parity + degraded-routing round (2026-08-30): churn=self on emitGrepReport and classifySkipHealth is this one edit window — each was edited twice within the round because the round's own quality-delta demanded the second pass (the parse_degraded= inline predicate and classifySkipHealth's errNodes test both re-routed through the ONE fileParseDegraded predicate hoisted to model.h, so the three degraded surfaces cannot drift). Final state carries no complexity/verbosity/duplication finding; selectorscopecheck 8 arms + degradedhintcheck 8 arms green. ack short-horizon-churn 4a7106e488a2aa80 7 cid=a74981bb688153d7 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 @@ -815,6 +852,7 @@ ack short-horizon-churn 5774f0f445361430 8 graphrag-recon idea #1 corroboration- ack short-horizon-churn 578fa051307418ee 5 cid=307a821a72c35f00 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 59f050855874875f 14 cid=dbe0701cc173e389 by=src/* R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. ack short-horizon-churn 5b224c7fe142bd56 37 cid=3352105024f32829 by=src/* lift-disclosure round (2026-09-10): applyStructuralExpansion/applySiblingLift's optional *LiftInfo out-param is the disclosure hook itself (api-surface contract-change, purely additive/default-nullptr per G5) - and the 4 short-horizon-churn(self) rows are the necessary --for/--pack-task integration points (computeLensRanking, forLensHeaderText, runForLens, packTaskBundleText) in files under active development; duplication/complexity/verbosity this round introduced were fixed, not acked | prior: issue #61 disclosure lane: the only gating row is short-horizon-churn on runForLens (churn=self) — the --for XML emitter is where the over_ceiling verdict has to be computed, so editing it again this week is the fix, not thrash. The predicate itself was extracted OUT of that body into the file-scope forLensOverCeiling beside its JSON twin, which is why the verbosity row dropped to sev=minor (+6 LOC, all of it the call and its pointer comment) instead of carrying the whole METHODOLOGY §9 argument inline. Gate: test/formaxtokenscheck.sh, written red-first. +ack short-horizon-churn 5c2c4a2b311b8dba 5 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 5d8f5288f0df10f7 95 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact ack short-horizon-churn 5e16b9596d9bd135 45 cid=743b6f0afb91f3cc by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: root-relative coverage round (verifier E1-E4 + two gaps the widened gate exposed, 2026-08-19): every gating row here is the SAME three-line pattern every verb in the original root-relative round already pays — a singleRoot bool, a rootPrefix, a rootAttr, and one ternary per path emission (the shape clones/prcontext/situ/mcp-path all carry verbatim). --tree (runStructureText) +8 ccx / +13 LOC and --quality-panel (writePanelReport) +4 ccx / +12 LOC are those lines plus the finding comment; forTaskText and packTaskBundleText are argument threading only. packBodiesJson api-surface 3 to 4 params is a DELIBERATE contract change: a defaulted trailing rootArg, identical in name, position and default to the one packSignatures/packBodies/packLego/packOutline already take, so the emitter family stays one shape and every existing call site is unaffected. churn=self/ambient is this change's own edit window. Payoff: 1340 absolute paths removed from four surfaces (tree 1212, analyze 85, panel 40, mcp-for 3) plus 5 in the pack-task JSON tail that the gate had been scoring on an empty document, and every single-root run now discloses its root exactly once. All red-first in test/rootrelcheck.sh ack short-horizon-churn 5e5cc30bcbc1fb63 5 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. @@ -827,7 +865,8 @@ ack short-horizon-churn 61d6cde8defa73ad 7 cid=5d0a21a4321d8300 OPTREMARKS F3 (d ack short-horizon-churn 61e5df9e1e40ff70 13 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack short-horizon-churn 623e9c51c095e307 3 S2b sweep-escalation lane: hooks/ripwire-nudge.sh was rewritten twice in 24h by the S2 meter lane and again here, so every meter_* function trips short-horizon-churn on any edit at all. The churn is the file's recent history, not a property of this change (the legend calls this kind preexisting by construction); the verbosity growth it came with WAS fixed, by splitting meter_classify_git and meter_classify_other out of meter_classify_bash. ack short-horizon-churn 624a465290b8a040 3 cid=7b2469337b9ad141 lane E close (terminality round A): run_editsuite.py: the ripwire half split into classify_ripwire_call; remaining rows are churn on the change's home -ack short-horizon-churn 639de1c3670999f9 23 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) +ack short-horizon-churn 6302e2e27e23bcde 4 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack short-horizon-churn 639de1c3670999f9 30 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack short-horizon-churn 649ed79cefcd6fc6 6 cid=a3636823a3a67d9f by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack short-horizon-churn 6652d5114718eb63 7 cid=497a477af2561c0a OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. 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 @@ -860,6 +899,7 @@ ack short-horizon-churn 7c0cabd952bf3bd0 5 R-J: genuine feature growth in emitGr ack short-horizon-churn 7c0e356e60b323ba 41 cid=02317a43a1806043 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 7cc5608fd1dba918 6 cid=d15d69ecd7b55322 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn 7d68e725e85e246c 5 cid=a38b17af7ad94ae5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. +ack short-horizon-churn 7d760d428aab46d1 6 cid=bb2c5ceed4e8efd8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 7e26e5533d486c7a 13 cid=aa596d9bb05d621d cap-disclosure lane (2026-09-10), --from-trace + --handoff: every row is this one change's own footprint. The gating churn=self row is renderTraceBlock, whose two emitTo format strings are exactly where the name_ladder_capped= attribute has to be written - the file is hot this week, so modifying its emitter at all reads as in-window thrash; nothing was rewritten, two attribute slots were added. partitionTraceFrames 14->16 is the ladder-total plumb (one local + one push into the parallel unresolvedLadderTotal vector). The api-surface rows are the new conditional-disclosure seams (NameLadder, TraceNameBinding, nameLadderAttr/hasNameLadderCut/ladderLegendOf, VerifiedFileRow/verifiedFileRow, kNameLadderLegend, kHandoffSymsCapClause) - all header-inline, all with their own call sites in the same commit. writeHandoffPacket got SMALLER: extracting verifiedFileRow removed its inner symbol loop, so its pre-existing complexity 57 and 240 LOC both fell below baseline instead of growing. Gate: test/tracehandoffcapcheck.sh, red-first against the parent binary. | prior: P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack short-horizon-churn 7ed8ad2c213537a4 24 cid=959b21f28b01efe9 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself ack short-horizon-churn 7efd6731993172f3 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. @@ -872,7 +912,7 @@ ack short-horizon-churn 815fbf65eea5df47 5 cid=98131c6d45bba16a OPTREMARKS F3 (d ack short-horizon-churn 8192a44ad5eb2510 3 cid=ff366a47a1cdb76d by=src/* member-variable round (card A3), side-table rule: symbols this round created (collectFieldUseSites, FieldUseAnswer, memberOwnerRefusal, declaredFieldSet, isInstanceFieldSite, dropFieldDefinitionSites, fieldCaptureKept) and touched twice within it while fields moved from ing.symbols to the IngestResult::fields side table under the orchestrator's rule; collectFacts/buildDefSpanIndex each carry ONE deliberate edit ack short-horizon-churn 81efad81c80fc1cd 10 cid=99f4094a45b32fa2 F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack short-horizon-churn 824e30c136361009 4 cid=11f0e44f31665844 wave-3 close: compactlegend.h self churn from replacing the hand-rolled startsWithSv (a 45-token clone of darkflags endsWithView in the ec5e3c3..HEAD delta) with std::string_view::starts_with at its 11 uses — a deletion, one day after L7 created the file -ack short-horizon-churn 82b1e3c6a4919914 16 cid=d50eb6215046e886 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn 82b1e3c6a4919914 16 cid=d50eb6215046e886 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn 82bf48718457b826 2 cid=36b75364752453aa L10: --doctor legend + blobs_floor= disambiguation (finding 6) — DoctorCacheStats gains capHit, doctorCacheStats sets it, runDoctor emits blobs_floor= and the new legend comment; short-horizon-churn and the verbosity bump are the direct, deliberate cost of that ack short-horizon-churn 83f27ab44a2fb8a7 2 cid=7b1641f001fe32cd P7 (terminality round A, lane R): droppedpositivecheck's verify_exact re-pinned to the FLAT --for --json sigs array (one row object per ranked symbol, no {p,symbols} wrapper) — this lane's own gate edit, not drift ack short-horizon-churn 8430c0a1b20d242e 13 cid=3edc3a9877090050 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. @@ -940,9 +980,10 @@ ack short-horizon-churn a7ec845d7950d0b5 6 cid=383e339bb184adff OPTREMARKS F3 (d ack short-horizon-churn a8b774025a21bdc6 83 cid=fa890c64b85771c2 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: M1: runBatchSub gains ONE defaulted parameter, compactLegend, so a batched slice is built by the same emitter path as its standalone twin (batchcheck (h) measured the divergence: 1,542 B vs 606 B). Both call sites pass it; --edit-check reports callers=2 incompatible=0. The params bar is 5 and this is 6, taken deliberately rather than threading a second struct through a 14-arm dispatch. ack short-horizon-churn a9f76a08efdb3d50 24 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. ack short-horizon-churn aa71fcdf69942431 66 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. +ack short-horizon-churn ab7737f3582352d5 4 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ab9b3f1db516af63 21 cid=0b950315073c2901 by=src/* M12 (capture-audit L9): path-spelling fixes — collectUseSites gained a root parameter (default-valued, back-compat) to root-relativize in_id=; runVerify grew from adding root=/verPathRel/the multi-root roots table it never had; short-horizon-churn rows are every function this finding's fix touched this session. | prior: A6 (survey card A6, agent-lsp): tested/untested partition on --impact/--callers/--callees rows, reusing the isTestSymbol-seeded lens computeQMetrics/--safe-delete already run (graph.h::testSymbolForwardReach/countTestedIn, shared, not duplicated per-verb). api-surface (2, contract-change): emitColumnarSymbolRows/printJsonSymbolRows gain one optional testReach pointer param (default nullptr, byte-identical on every pre-existing caller) so the columnar/json dialects can carry the same tested= column the XML dialect carries. complexity (1, runCallHierarchy 80->83): the partition's counting loop itself was factored out to graph.h::countTestedIn (shared with --impact, verified this ack run no longer lists emitColumnarSymbolRows/runImpact, which carried the identical loop before extraction); the residual 3 points are the hop_tested=/hop_untested= attribute wiring on an already-large pre-existing dispatcher (its own header comment already names it 'this file's largest dispatcher') - splitting it into per-dialect emitters the way --impact already is would be a correct follow-up but is a materially larger, separate diff. duplication (2, testSymbolForwardReach vs situ.h::testSeedForwardReach / vs csharpInFileTestScope, 38/28 tokens): the shared seed-collect-then-forwardReach shape now lives in ONE template (graph.h::seedForwardReachIf) that both isTestSymbol- and isTestPath-seeded callers delegate to in one line each - the residual similarity is between two intentionally-DIFFERENT predicates the L8 comment on computeQMetrics explains why cannot be merged (a Rust in-file cfg-test mod has no isTestPath file at all), and csharpInFileTestScope is an unrelated short predicate the token-shape coincidentally now resembles post-extraction. short-horizon-churn (8) and verbosity (2): the partition touches --impact's three dialect emitters (XML/columnar/JSON), --callers/--callees' single dispatcher, the shared row helpers, and the MCP impact twin (mcpclidiffcheck parity) in one change - one feature landing across its natural surface, not incidental edits. Full assigned gate list green (reachcheck/callerscheck/impactimportcheck/testedreachcheck/testgatecheck/testgatepagecheck/testgaterefusecheck/graphlegendbudgetcheck/floormarkcheck/legendcoveragecheck/manifestcheck/mcpclidiffcheck), determinism + xmllint clean. ack short-horizon-churn ac88b70b8c51cc70 14 L2 stale-ack disclosure: unavoidable growth/self-churn on runQualityDelta and qualityDeltaJson, the two pre-existing quality-delta dispatch hubs every new axis has to touch; logic already extracted to quality.h (staleForXxx/staleAcksXml/staleAcksJsonArray) to minimize the added footprint -ack short-horizon-churn ac9a2be19aa5cd79 151 cid=a601a09adfcfe345 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn ac9a2be19aa5cd79 154 cid=842f93fca4a9a61f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. ack short-horizon-churn adc4f10887c0a91d 20 cid=552e72bf148f6ab9 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack short-horizon-churn ae1f15d03c3d4faa 9 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 b0faf2a94fa4cc2d 7 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. @@ -956,7 +997,7 @@ ack short-horizon-churn b4e161be7a843fd6 5 cid=2871d9a64ac68a01 OPTREMARKS F3 (d 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 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 151 cid=9a5d0ccafb5fee94 answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn 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. ack short-horizon-churn b7da91c05624ab1b 6 cid=12ba71ea2a2cbf3d OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn b7e9e25ba704623a 2 cid=7a87da721dc19be4 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. ack short-horizon-churn b8c5550b5e3dc150 6 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) @@ -976,6 +1017,7 @@ ack short-horizon-churn c0d15239c2c4709d 24 cid=acf0020a7976fbe1 2026-09-06 stra 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 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 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). @@ -995,15 +1037,16 @@ ack short-horizon-churn ce968bdf8848c214 13 cid=3c5d6c07cbc4425e OPTREMARKS F3 ( ack short-horizon-churn d03215f48bec3886 3 cid=c7f3e74cf288dfc2 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper ack short-horizon-churn d05476877df0016e 6 cid=94feac15a58025e5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d3afca728392d688 6 cid=5765fc3aca7af273 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. -ack short-horizon-churn d42c85b67bd0956f 15 cid=346b0c28d573003f L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) +ack short-horizon-churn d42c85b67bd0956f 28 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) ack short-horizon-churn d44595768a7cf3af 2 cid=21539523996cfb13 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. ack short-horizon-churn d557a0077677ebd4 39 cid=7e6e8b041d54e368 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical -ack short-horizon-churn d63db6944aa504a7 35 cid=7d756152fbee4105 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. +ack short-horizon-churn d63db6944aa504a7 35 cid=7d756152fbee4105 lane/helptask-precision 2026-09-10, MCP no_route (audit F-R1-07): all eight gating rows are this one change and nothing rides with it. api-surface forTaskText 4->5 and packTaskText 5->6 params: ONE DEFAULTED bool each (noRoute), so every pre-existing call site compiles unchanged and was verified to; the alternative — a second overload per verb — is the clone seam this repo removes rather than adds. complexity +4 on each of the same two: the four !noRoute gates are a MIRROR of verbs_for.h's own four (cfg.noRoute gates the shape demotion, the mention anchor, the co-change prior and the route note), and collapsing them would be the MCP dialect deciding for itself what --no-route means — the exact drift mcpforparitycheck exists to prevent. Both were already far over the ccx bar (55/29) before this change; decomposing forTaskText is its own round. short-horizon-churn churn=self on dispatchMcpLine, kMcpVerbFields, forTaskText and packTaskText is the footprint of having edited four symbols this window already touched. FIXED rather than acked in the same pass: the verbosity row on dispatchMcpLine (1376 -> 1387) is gone — the second hand-rolled five-line boolean accumulate was replaced by ONE guarded boolArg reader that post_check now shares, the rule intArg already states for the numeric fields, netting the dispatcher SMALLER than before. Manifest re-anchored 41300 -> 41650 for obliged schema bytes only: descriptions are BYTE-IDENTICAL at 19632 B | prior: C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn d6e55b78e3a5fdda 7 cid=31161fe2ed1af5a5 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d8aaf90801200a68 13 cid=d059a10443da1f80 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack short-horizon-churn d9990dc7492a8bb2 20 cid=4646606f648e387e by=src/* member-variable round (card A3): kUsesLegendOpen gains the one-sentence pointer to the member form, buildDefSpanIndex zero-widths field spans so containment attribution is byte-identical, kParserVer 74->75 for the new SymKind::Field + member use-site capture — each a single deliberate edit on a symbol other rounds touched recently ack short-horizon-churn d9e6c64c181ffc32 8 cid=5fb1e641981216da by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). 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 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. @@ -1029,6 +1072,7 @@ ack short-horizon-churn e750b88b46822476 2 cid=50e93e1770abeaaf by=src/* Phase 5 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 ed0cbdfe7fa72e8c 154 cid=f42bf8683228ffff C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack short-horizon-churn ed10de1685178c0e 12 cid=b144cdefb89bd164 R1 (wave-2 verifier): the redaction-marker write gate rewritten from a payload substring scan to a comparison against the bytes it would replace — short-horizon churn on the five symbols this round has been editing repeatedly, not new debt; the complexity/verbosity of all three write surfaces is unchanged or lower after the shared redactionMarkerRefusalFor helper | prior: A5/A7: short-horizon churn on editplan::prepare and ::receipt is this fix round itself -- five assigned defects on one small surface, committed one per item, so the same handful of symbols falls inside the churn window repeatedly. churn=self, not instability in the code. The duplication row this pass also raised (withinDir vs rw::pathIsUnder) was FIXED rather than acked: both that helper and a hand-rolled lexicalNormalize were deleted in favour of the existing resolve.h primitives. ack short-horizon-churn ed4b3f43f7f19909 12 T3 disclosure-gap fix 2026-08-22: verbosity/churn on the two emitters + harness trace persistence are the registered disclosure's own bytes and comments; the gate-helper clone follows the self-contained-MCP-gate convention (every mcp gate carries its own mcp_call) ack short-horizon-churn eea83c3db0f03d69 39 cid=95634463181e8692 P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: deep-tail lane (docs/EVALS.md Deep-tail serving; gate test/deeptailcheck.sh): the rank fact + file-grain tail land on every lens serving path at once, so the serving emitters carry the lane's own diff. api-surface jsonSigRowHead 6->7 = the defaulted globalRank param (0 = key absent; every existing caller source-compatible). complexity/verbosity runForLens +4/+39 and emitForLensJson +13 = the four seams a charged section costs (render, ladder charge, est charge, emission) after the fit logic was already extracted to renderForFileTailXml/forLensJsonTailStanza; forTaskText +17 = the MCP twin's parity wiring. churn=self rows are this one lane's diff on the emitters it owns, not thrash. The tail/r fit logic itself lives in serialize.h free functions, gate-covered red-first vs d8e257d. @@ -1054,6 +1098,7 @@ ack short-horizon-churn fbdb0484f0707ba5 21 cid=ec37ab1a24846677 P1.2 is the sec ack short-horizon-churn fce48dedc40bc8eb 18 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 fd4cc5c6c0ad17d9 8 cid=9f660d42ad009523 C4 recall budget spreading: recall.h's budget loop was rewritten load-allocate-emit, which necessarily edits committed lines the 2026-09-04 capture-audit round last touched. The churn is real and correctly measured; the edit IS the fix for the defect those lines carry (recallbudgetcheck 8.1/8.2/8.9). Nothing to refactor away. | prior: markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean ack short-horizon-churn fd9dd2a5976fbf21 4 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 ff2ba74637bbbebf 6 cid=16a96df9744925a7 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 verbosity 05b5f1acf3a7e880 593 cid=10f4dfeb9f1d70cd P1.2 is the second half of one plan item whose first half landed in the immediately preceding commit of this same lane. scopeDisclosure gains a fifth parameter (the diff-expansion count) with one call site, both in this change; the churn=self rows are these symbols being finished, not thrashed. | prior: the scope partition is a feature added to a documented 600-line sequential pipeline. Every separable part is already its own named symbol: refuseUnusableScope, partitionByScope, refuseForeignAckSelection, quality::scopeDisclosure and kScopeLegend. What is left is the pipeline's own sequence. | prior: scopeless-fold lane: runQualityDelta verbosity 462 -> 474 is the MANDATED DISCLOSURE, not drift. test/legendcoveragecheck.sh refuses any first-screen attribute that its verb's own legend does not define, so the two new identity attributes (acks_rekeyed_by_scheme=, scheme_ambiguous=) had to be defined there or not emitted at all. The +12 lines are that definition, and they are prose inside a string literal in a function that was already 462 lines of overwhelmingly literal legend text. Extracting these legends into named constants would genuinely shrink the function, but every legend in main.cpp is written inline; hoisting one of them alone trades a size number for an inconsistency the next reader pays for, and several gates grep the legend text in place. Recorded as the cost of the disclosure, with the extraction left as its own refactor across ALL the legends rather than a drive-by on this one. ack verbosity 060a064b6ffa7775 621 cid=605cbb1f768828e0 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack verbosity 070de5367d6a5be0 87 wave merge 2026-08-20 (harvestexec): run_swebench_harness +11 LOC, from lane/outcome-harness-fixes. Every added line is DOCSTRING, and the trade it makes is the one this project wants: a 15-line 'TODO-verify' block listing what nobody had checked was replaced by a shorter VERIFIED block naming what was checked against swebench 5.0.2 on 2026-08-20 (CLI entrypoint and flags, predictions schema, report filename and the model_name_or_path slash-substitution that does not fire, resolved_ids at harness/reporting.py:154) plus a KNOWN UNFIXED GAP paragraph disclosing that the published eval images are x86_64-only, so an aarch64 daemon silently rebuilds locally and produces numbers not comparable to published ones. The executable change is one defaulted parameter (dataset_name). LOC counts the disclosure at the same rate as code; deleting it would restore the number and re-hide a result-invalidating gap, which non-negotiable 3 forbids. @@ -1083,6 +1128,7 @@ ack verbosity 2b0d39faec60fc08 91 R-R root-relative emission lane: threading the ack verbosity 31105421c3ce88ae 249 cid=86626441189f2998 2026-09-06 stranger-audit fixes: doctor compares bytes and fails off-PATH, html names its root+commit+version, at= carries +shallow, edit-lock sweep — the contract change (writeDocumentShell takes the title) and runDoctor's two new verdicts are deliberate; the churn rows are this edit itself | prior: F6 (lane F): runDoctor +14 LOC is one emitted attribute (volatile=) plus the comment recording the three rounds of gate flake it retires and why removing the fields would be worse; runDoctor is a 223-LOC row emitter already far over the bar. churn=self on runDoctor and on shapingflagcheck's fnorm is this session's own edits inside one window while the F6 disclosure converged (declare, then re-pin the two determinism gates onto the shared helper). ack verbosity 3298be65bf6058ef 1265 cid=7327ba02472bb8c7 rich-ingest promotion for the eval verbs: --eval-retrieval/--eval-mined/--eval-skills now request captureValueUses so lexicalScoresTiered takes its persisted-stats path instead of re-tokenizing the corpus per query (94% of eval user time). short-horizon-churn rows are this session's repeated edits to those functions, not instability; verbosity on dispatchMain is one added comment. Scores byte-identical, postingscheck ALL PASS. | prior: M1: self-churn on the four symbols this change edits — printUsage (the --legend help text now states the MCP default), kMcpValueFields (the legend field description, the one place every declaring tool's schema states the posture), packTaskBundleText (dropped_positive moved from the ledger prose to the ctx root) and dispatchMain (--batch --legend=compact now compacts its sub-answers). Short-horizon churn measures edit recency, and these are the edits. ack verbosity 3561d0281d324276 277 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path +ack verbosity 380b7de5df1cfd73 100 cid=5c6aaf66598466c8 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 verbosity 3877dd1e9b4ae997 66 cid=c7ed26e6f4cf641f P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack verbosity 38c814a492cc9c8a 176 cid=a9f0f1c829b158bd by=src/* Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: A2 (dropped_positive, 2026-09-03): emitForLensJson gained the droppedPositiveStanza, mirroring the existing overCeiling/notesStanza envelope-key shape; self-churn is this round's own fresh edit. ack verbosity 3a0425468e6a18cf 157 cid=e6e77cfc3574d18b capture-audit 2026-09-04 wave-2 merge: L10b finding 6 (kHistoryProbeLegend spliced conditionally on res.history, the attributes defined for the first time — historyoraclecheck) + L6 H14 (filter= echoed on the root; the MCP twin's selector parity — mcpattrparitycheck SELECTOR). Two disclosures on one page writer | prior: L10b finding 6: legend clause on --whereis/--doc-drift, conditional on --with-history @@ -1090,7 +1136,7 @@ ack verbosity 3b19cc3d8996c3b2 167 cid=e9c5f629b2a2926a by=src/* lane/n2-i punch ack verbosity 3ba0d3f31d319672 77 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 verbosity 3be1c13661e5a63c 74 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 verbosity 3c07d993bfdbce53 197 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS -ack verbosity 3d87404c1cdf50ec 164 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. +ack verbosity 3d87404c1cdf50ec 180 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack verbosity 3e0a074094789d60 79 cid=3256a8c50529082d Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. ack verbosity 44b41056b999b501 111 D2 audit-regression fixes: renderWholeFiles/chooseExpandServe gain a shaping param each (compress + pack-budget composition, both single-caller, callers updated in the same edit); anchor-row loop and disclosure comments add the LOC/ccx; churn is this lane's own self-churn on the four symbols it edited ack verbosity 45b52ada32f63c11 565 cid=877f2f471ee33bc4 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: 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 @@ -1110,7 +1156,8 @@ ack verbosity 5b224c7fe142bd56 990 cid=6c2e2b906ce06245 by=src/* round ec5e3c3.. ack verbosity 5d8f5288f0df10f7 351 cid=0e1c73cb2c8e5166 L10b finding 10: --version's built_from= label matches --doctor's own attribute for the identical fact | prior: 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 verbosity 5ec38fbd414fa4d4 169 cid=6f20993f1b475898 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: 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 verbosity 6031be13b40a1b6f 205 cid=58496e25cbf67431 round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites -ack verbosity 639de1c3670999f9 183 cid=bbf7c6fe9dbbe217 wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement +ack verbosity 6302e2e27e23bcde 64 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. +ack verbosity 639de1c3670999f9 185 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) ack verbosity 685ade09a168535e 96 cid=124bb29c88050ae1 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | 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 verbosity 6acbcaa854eada23 289 cid=e5c009ae9a0d8393 preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. ack verbosity 7004309695fb79f1 265 2026-08-15 harvest wave-level pass (orchestrator): 12-lane wave measured as one delta vs origin/main 4b9386c per verifier finding 6. All 21 gating rows triaged individually: emitGrepReport/grepHitsJson/runCallHierarchy/runDefaultMap/collectSources/printUsage/Config/runMcpHttp = feature absorption by design (grouping+boolean+corpus disclosure, file-root, bodyless_defs+legend, estimator guard, new flags), each converged and gate-verified at lane level; short-horizon-churn rows = single-wave multi-lane edits of shared hubs, process artifact; sym=main rows are main.cpp::main growth mislabeled to analyze.py by the bare-name canonId collision (path-qualified keying fix d593de3 still unpushed). emitGrepReport cx 25->63 flagged as W2 split candidate in PLAN round record. @@ -1127,6 +1174,7 @@ ack verbosity 7fd074f7cb0b0946 148 cid=0b7c1148e7beac79 lane hb-R3 (harvest-B ca ack verbosity 80a2f3a7c92fbed4 555 cid=df52b38ce1ca6bb9 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: Phase 4b lane (Rule 2c class-name receiver, 2026-09-03): kParserVer churn=self is the 75->76 bump this round's one ingest fact (Python parameter names as empty-span VarDecl veto evidence) requires; Narrower verbosity 437->481 is the feature — methodOnTypeOrBases hoisted OUT of rule2bFieldRecvType (2b shrank by the same body) and reused by the new rule2cClassNameRecv, each with the WHY comment the house requires. buildGraph's four copies of the narrow-apply loop were folded into one narrowTo lambda in the same change, taking its complexity BELOW the baseline; astropy map and census byte-identical across the fold. ack verbosity 81fbe59b4a35659b 171 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 851e83b4505f10f6 129 cid=1730ab232c9a0037 by=src/* A2 (dropped_positive, 2026-09-03): collectJsonSigEntries gained the rank + positivesContentSkippedOut trailing params and the three-way positive/content-skip/budget split droppedPositiveCount needs — the minimum surface to feed the shared arithmetic; see docs/EVALS.md A2 registration. +ack verbosity 86db2ff4e22cae54 70 cid=b9c9829a2aac834e 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 verbosity 89b3fd14c192c899 1494 cid=bf9296f0f9d8d2a3 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) | prior: wave merge 2026-08-20 (harvestexec): TWO seams in buildGraph, both from this wave, measured together at the merged head (cx 753->764, LOC 1327->1360). (a) The RefRole::Type namespace gate, from lane/resolver-precision: +8 cx / +23 LOC, ONE inserted block - a stable in-place filter over the assembled candidate set (12 lines of code) plus the 9-line derivation comment above it, plus a 3-line re-route of the isClassLike lambda through the same predicate. Not extracted to a helper on purpose, and the reason is the gate's own subject matter - on THIS loop the predicate is a provable no-op (the loop admits only role=Call, un-narrowed by resolve.h doctrine, and role=Macro, which retagMacroCallReferences already proves uniquely macro), so the block exists to be the ONE seam a future round edits, and hiding it behind a call would move the lines without removing them while making that edit invisible. test/nsfiltercheck.sh arm 2 is the executable form of the no-op claim and fails if the narrowing ever starts biting here. The measurable effect lives in contextratio.h's all-roles resolution, and the role that reaches it is RefRole::Extends, NOT RefRole::Type - collectFacts continues on Type 28 lines before it calls resolveCandidates, so a Type reference can never arrive there, and what keeps a type mention from spraying across same-named functions is that continue, not this predicate. What the predicate keeps from spraying is a BASE CLAUSE: on test/nsfilterfix, 'class Derived : public Handler' binds to both 'class Handler' and the free 'int Handler( int )' without it (ents 1->2, amb 0->1). Corrected 2026-08-20 by adversarial verification (V-5): the two source comments this ack leaned on named the wrong role, and test/nsfiltercheck.sh arm 5 now pins the real effect on the real role - the gate is red under full removal of the narrowing, which it was not before. Acked separately below. (b) The S5-E compose lang guard, from claude/kind-kepler-0c9b90: +3 cx / +10 LOC, one langCompatible branch plus its rationale comment, so HAS-A resolution applies the same language gate as every other admission site; before it a C++ member bound to a Python/TS same-named class and the duplicate defeated the (ownerSym,fieldName) dedup. Recorded red-first by test/composelangcheck.sh (mixed-lang fixture test/composelangfix, C<->C++ bridge control kept green). Cost is bounded and one-time for both: buildGraph does not grow again when a role or a language is added, only when a seam is. ack verbosity 8a92173ded649e17 158 cid=d49e411bf180293e by=src/* P7 (terminality round A, lane R): short-horizon churn on the lens legend clauses (kForFileTailLegend/Compact, kPackTaskBundleLegendBody: 'rows in r= order, p= the file') and on the two packers this lane rewrote (packSignatures, sigRowHead) — the P7 shape change itself, not drift; gate test/forrankordercheck.sh | prior: A2 (dropped_positive, 2026-09-03): packSignatures gained one trailing droppedPositiveOut out-param and the post-ladder accounting inside the rank-adaptive branch — same shared-arithmetic reason as its JSON sibling. ack verbosity 8c07caf47e2f33a4 134 cid=0002f98cf93b4127 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. @@ -1146,6 +1194,7 @@ ack verbosity a3a317502ee383bc 77 R-E CORRECTION lane (2026-08-19), the W2-E roo ack verbosity a783c75feea9f253 114 cid=7345e4b8de2407c9 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit ack verbosity a8b774025a21bdc6 338 cid=2fdcb1fc6fa90041 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: P17 slice+edit_check join MCP batch: runBatchSub gains two else-if arms (each calling the standalone builder, sliceText/editCheckText, so a batched answer cannot drift from the live one) plus three argument reads; dispatchMcpLine gains the post_check boolean read for the three edit verbs. Both are dispatch chains growing in kind. runBatchSub is NOT decomposed in this commit on purpose: lane L6 is changing this same function's sub-query GRAMMAR in this wave, and restructuring it here would guarantee a merge conflict over a concurrent lane's work — the decomposition is recorded as a follow-up, not skipped silently. ack verbosity a9f76a08efdb3d50 84 cid=93c35f3d948f24a3 F3 (lane F): runAffected +4 LOC and printUsage +3 help lines are exactly the --affected test-partition fix and the sentence that documents seed_test_files=/seed_kind=. Both were already far over their verbosity bar before this change (printUsage 1478, runAffected 80). The short-horizon-churn row on runAffected is churn=self — this session's own two edits to that symbol inside one window while the fix converged — not accumulated debt. | prior: capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept +ack verbosity ac9a2be19aa5cd79 653 cid=842f93fca4a9a61f 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 verbosity b496a273ae1564ef 70 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 verbosity b8c5550b5e3dc150 65 cid=200408e3ce35c6b8 lane T 2026-09-05: install.sh --hook banner re-worded to disclose the v3 capture (Edit/Write targets, MCP symbol/file arguments); the matcher rewrite is the fix for MCP rows being invisible (hookcheck section 14) ack verbosity bb42e692522f57ff 101 cid=aa3e0713df809f6d capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement @@ -1160,9 +1209,9 @@ ack verbosity c840f9a6ec2e6c6b 73 WAVE-2 close (2026-08-19), finding 3 of 3: the ack verbosity c8c7bb0104aa16b8 76 cid=d54042b64ff7b78e H11 (lane ca-L2): writeBaseline gains a DEFAULTED absorbedGating parameter so a dirty pin's absorbed count reaches the sidecar; --edit-check reports incompatible=0 (both existing callers still bind) | prior: 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 verbosity ca97a4b6bf07887b 653 cid=b2c9107cb2d8ac03 Same three sites as the complexity rows and the same reason: LOC grew where a disclosure or a real computation landed (dispatchMcpLine +71 over five pagedResult arms and the legend refusal; symbolQueryJson +26 for the attributes the CLI twin has always carried). None of it is repetition a helper would absorb. | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity cb7342964b38db9c 112 cid=3af54969638510bc by=src/* member-variable round (card A3): usesText grows the Owner.field member-selector branch (resolveFieldSelector + bare-name refusal listing spellings), the registered contract of this round | prior: lane/r10-cheap-buckets (r10 GitNexus fix round, LB-A + LB-G). SIX gating rows, ONE lane, read one by one before acking. THREE api-surface contract-changes, all deliberate parameter additions that ARE the feature: (a) mcpverbs usesText 2->3 params, taking McpPageArgs exactly as impactText already did, because the MCP uses verb gained the same default site cap as the CLI and an MCP-only agent that reads capped=1 needs a hatch it can reach (mcpclidiffcheck LENS 1 pins the two surfaces' root-attribute sets equal, so capping one and not the other is a divergence, not a saving); both dispatch sites AND kMcpVerbFields updated in the SAME commit, verified by mcpclidiffcheck/mcpverbscheck/usescheck green. (b)+(c) serialize packSignatures 17->18 and packSignaturesJson 11->12, both taking a trailing hasRelevanceFloor bool, default false so every non---for caller is byte-identical (verified: default map, pack-task, expand, exemplar, recall, hotspots, callers, grep, impact all unchanged). The flag cannot be replaced by passing a smaller topN, because those emitters read topN==0 as ALL, so a query nothing scores on would emit the whole corpus. THREE verbosity rows are the new code itself, already cut twice in this lane: duplicated rule bodies hoisted into relevanceFloorCut/pathTierIndexOver/compareTierThenPath, then the restated rationale moved to those helpers' headers - together taking gating from 13 to 6. What remains is runForLens +10, runCallHierarchy +11 and usesText +14 lines of genuinely new behaviour (the floor cut and its note plumbing; the tier index, the page window and the conditional legend clause). Splitting runForLens is a real refactor of its own - it was 658 lines before this lane touched it - and does not belong in an output-composition fix round. -ack verbosity d42c85b67bd0956f 192 cid=65539b6b8b8297c6 round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) +ack verbosity d42c85b67bd0956f 214 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack verbosity d44595768a7cf3af 106 cid=ad934f87aaaed8df preloaded-corpus hoist: lexicalScores/lexicalScoresTiered gain one optional defaulted preloadedFileText param so a caller scoring many queries against one tree reads the corpus once instead of per call (--eval-retrieval was ~11.8M file opens/run, 48% of its CPU in the kernel). api-surface rows ARE the intended additive change; short-horizon-churn is this session's own edits to those two functions, not instability. Scores proven byte-identical on an identical tree. -ack verbosity d63db6944aa504a7 1330 cid=6e21514d6315a8e7 round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through | prior: F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. +ack verbosity d63db6944aa504a7 1388 cid=1943300f4961480a 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: round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through ack verbosity dbe6ed5269d328a4 325 cid=f7116f48272d6777 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (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 verbosity dd02b378ae6b5b75 126 cid=9ec35c817a609c0f capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: L10 finding 10: writeEnsembleReport/writePanelReport now build conditional unavailable=/unavailable_why=/uncounted=/unavail= attribute strings instead of unconditional printf %s slots, so an attribute absent-means-none instead of printing ="" — the complexity/verbosity growth is that conditional-building cost ack verbosity dd627540f10bba76 643 cid=008b513e002b71fc by=src/* round ec5e3c3..HEAD, the task lenses across V1 N1 (est_tokens on the root), L10b (route= trim, doc_mentions=), L6 (budget_tokens=), V2 F2/F5/F6 (over_ceiling on every rung, smallest-ceiling rule, route bracket) and L7 P3/P10 (r=1 next=, one outer partition legend): five lanes grew runForLens/packTaskBundleText/fromTraceBundleText/computeLensRanking past each other's acked magnitudes | prior: capture-audit 2026-09-04 wave-1 close, lane L4 (floor + paging vocabulary, lane-L4.md): body growth of the emitters that gained M2's capped=1 => paging-quintet disclosure, M11's priced root (est_tokens=/budget_tokens=/over_ceiling=/withheld_rows= on pack-task/from-trace/handoff), finding 4's ladder (packSignatures), H5/M15 floor + gauge and rule-4 count_capped/any_of findings_capped (runDefaultMap/runLint; runLint also carries L10's compiled= mapping). Each is the disclosure plus its ceiling arithmetic, pinned by estchargecheck/truncvocabcheck/collectioncapcheck/floormarkcheck @@ -1183,69 +1232,4 @@ ack verbosity f1aa10922e41f258 84 cid=6670a895f0520c6a E3 (terminality round A, ack verbosity f8d747d123b4143d 94 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 verbosity fce48dedc40bc8eb 63 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 verbosity fd4cc5c6c0ad17d9 154 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean -ack api-surface 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 1c11c9480374c3a4 4 cid=fc37866fa1022267 by=src/* M13 paging/budget parity: each of these nine gained exactly ONE trailing DEFAULTED parameter (an McpPageArgs window, a token budget, or the legend posture) so its MCP twin can honor the flag its CLI twin already honors. Additive by construction - every pre-existing call site compiles unchanged and was verified to - and the alternative, a second overload per verb, is the clone seam this repo removes rather than adds. | prior: round-4 F-03 (MCP for vs CLI --for candidate-pool divergence): all three gating rows are this lane deliberate footprint. api-surface contract-change forTaskText 4->3 params is the FIX, not a cost: the removed parameter was int topK, and both call sites (the for dispatch arm in mcp.h and the batch sub-verb) fed it the SERVER-WIDE --top-k whose default is 200 — the ranked MAP row cap, which --for is documented to ignore (cli.h honorsTopK). So the MCP verb ranked a 5x wider candidate pool than its CLI twin on every call an agent could make (dropped_positive=169 vs 11 on parse arguments over this repo, and a different served symbol set), and the for tool schema exposes no cap that could reach the CLI behavior. A knob only ever fed the wrong value is not fixed by a better default; removing it is what makes the two dialects unable to drift again. Both call sites updated in the same commit; no external consumer exists (header-inline, MCP-internal). The two short-horizon-churn churn=self rows (forTaskText, runForLens) are the footprint of having edited functions this round already touched. The cap itself now lives ONCE as serialize.h kForLensDefaultTopN, read by the CLI lens and the MCP verb alike. Gate: test/mcpforparitycheck.sh, run RED first against a binary carrying only the new constant (arms 1/2/4 failed: MCP pool 200 vs CLI 40, 19 CLI-served rows absent from the MCP set, --top-k=5 vs 400 not byte-identical). droppedpositivecheck arm #6 pinned the old MCP head and is re-derived in the same commit with its reasoning. -ack api-surface 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 3d87404c1cdf50ec 3 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 5c2c4a2b311b8dba 4 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 86db2ff4e22cae54 6 cid=b9c9829a2aac834e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 95cc88ca4aab7039 4 cid=25c411d4871bda46 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface c1e1d72154d6784f 5 cid=c495b60a16e1ce45 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface 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 api-surface ddc3e2a475f782a1 4 cid=4333f578e348e95f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface e57abf94a3b1f3a4 5 cid=788aa47a3ec3d3f2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 26e3f9a6bff9ea57 0 cid=942faaac6ddd5389 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 659af2613fd4ba75 0 cid=a69e85d30342478d C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 73690330b833c9fa 0 cid=e5b6815c0a360d29 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 7723ed789a1c9c09 0 cid=7886e44cd18fba0b C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 79cf899b082e5c8d 0 cid=99549639c3506907 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol 882ef9b9b64fd2c1 0 cid=62a45d4a835024bc C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol a10ea8d3bdca1dfb 0 cid=a64af151dfacd435 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol c26a94415f28768f 0 cid=a0746b8e350afbd2 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack api-surface:new-symbol c378ae8278c2ec12 0 cid=d22db6e5cdfe9bfe 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 complexity 1c11c9480374c3a4 51 cid=948595eb3858d81a F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). | prior: capture-audit 2026-09-04 wave-2 merge: L6 H14/M13 (confidence=/margin_pct=/at= on the MCP for root, budget_tokens=, the lens= declaration) + L10b finding 9 (route= trim) + the merge-fix that reconciled them (mention_anchored=/doc_mentions= served with the CLI's note wording, est_tokens= declared in lens=, and the CLI's confidence/at= sig-charge exemption applied so a disclosure never costs a ranked row — mcpforparitycheck (2) measured the row loss). Three conditional splices, each the CLI twin's exact rule; gates mcpattrparitycheck/mcpforparitycheck/budgetpolicycheck -ack complexity d63db6944aa504a7 527 cid=1943300f4961480a 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: F8/F9: dispatchMcpLine grows by the mixed-array refusal (the top-level element classification and its sentence) and the legend presence bit. Both are branches at the one place the server decides a request's shape; moving them out would put a request's validation somewhere other than where the request is read. -ack complexity ddc3e2a475f782a1 18 cid=4333f578e348e95f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack duplication ec3f7b5523723637 110 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params 49e172c2aa455e68 6 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params 86db2ff4e22cae54 6 cid=b9c9829a2aac834e 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 params ab7737f3582352d5 6 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack params d42c85b67bd0956f 7 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 083de06af84b3a87 30 cid=82bfd94f1c162607 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 1520fa02411735c3 4 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 1624b02e9104560e 154 cid=85e3075128763497 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. -ack short-horizon-churn 1c11c9480374c3a4 73 cid=948595eb3858d81a by=src/* F5 (lane F): forTaskText +1 complexity is the one guard on the priced splice (an empty document has no root to splice onto), and +14 LOC is 4 lines of code plus the comment recording WHY the wave-2 declaration was replaced by a served number. No new estimator: it calls serialize.h's pricedRootAttr/spliceRootAttrs, the same pair --handoff, --pack-task and --from-trace price through. churn=self on forTaskText and on the gate's band15 helper is this session's own edits inside one window (band15 gained an optional arm-tag parameter so the new arm reports under its own number rather than duplicating the band function). | prior: L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites -ack short-horizon-churn 3478654139c90f0f 4 cid=789faaec74b2ce26 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 380b7de5df1cfd73 6 cid=5c6aaf66598466c8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 3d87404c1cdf50ec 88 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 49e172c2aa455e68 4 cid=0ece6180949ad4aa C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 5c2c4a2b311b8dba 5 cid=264993eba8c648bd C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 6302e2e27e23bcde 4 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 639de1c3670999f9 30 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) -ack short-horizon-churn 7d760d428aab46d1 6 cid=bb2c5ceed4e8efd8 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn 82b1e3c6a4919914 16 cid=d0582bc76ff56c3e C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. -ack short-horizon-churn ab7737f3582352d5 4 cid=47b3fc03a5d497f1 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack short-horizon-churn ac9a2be19aa5cd79 154 cid=842f93fca4a9a61f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. -ack short-horizon-churn 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. -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 d42c85b67bd0956f 28 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: L10b finding 8: --situ distinguishes clean-tree from changed-but-symbol-free wording (small branch added) -ack short-horizon-churn d63db6944aa504a7 35 cid=1943300f4961480a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. -ack short-horizon-churn 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 e514a69013d0934c 59 cid=9f0bcb151f03e21f L10b finding: route= trim (no leading space+bracket), one-line change in each of the three routeNote construction sites -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 ff2ba74637bbbebf 6 cid=16a96df9744925a7 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 verbosity 380b7de5df1cfd73 100 cid=5c6aaf66598466c8 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 verbosity 3d87404c1cdf50ec 180 cid=15a146f0ee876b3f C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. -ack verbosity 6302e2e27e23bcde 64 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. -ack verbosity 639de1c3670999f9 185 cid=ffcb81de788c329a C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: wave-3 close, H7 hosts: runCrossRef hosts the --plan and --stray-content refusal sites the fix routes through the shared sentence (verify-wave2 lanes edited it days earlier — the churn is the fix's, self) -ack verbosity 86db2ff4e22cae54 70 cid=b9c9829a2aac834e 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 verbosity ac9a2be19aa5cd79 653 cid=842f93fca4a9a61f 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 verbosity d42c85b67bd0956f 214 cid=44e97a2de5235603 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut -ack verbosity d63db6944aa504a7 1388 cid=1943300f4961480a 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: round ec5e3c3..HEAD, the MCP door across L6 (batch two-grammar), L8 P9 (post_check on the edit verbs), V2 F3/F7/F8/F9 (receipt keys, batch attribution, top-level array reader, legend present-but-empty) and L7 P1 (legend on 16 verbs): dispatchMcpLine/runBatchSub/runCliEdit are the one dispatch each of those lanes had to pass through ack verbosity ff2ba74637bbbebf 89 cid=16a96df9744925a7 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. diff --git a/docs/TUNING.md b/docs/TUNING.md index 898bebd0c..40a80a2a7 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,18 +14,18 @@ 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 | | --- | --- | --- | --- | --- | --- | -| 125 | 124 | 112 | 12 | **37** | 75 | +| 126 | 125 | 112 | 12 | **37** | 75 | The first two columns are not the same number, and the gap is not a rounding: `src/` holds -**125 cap declarations** under **124 distinct names** (`kRowCap` declared in more than one file). The -sweep patches by NAME, so `112 + 12` accounts for the 124 NAMES — not the 125 declarations. Quoting -"113 of 125" would be wrong in both halves at once, which is exactly the shape of error a +**126 cap declarations** under **125 distinct names** (`kRowCap` declared in more than one file). The +sweep patches by NAME, so `112 + 12` accounts for the 125 NAMES — not the 126 declarations. Quoting +"113 of 126" would be wrong in both halves at once, which is exactly 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 125-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 126-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. From 5e1ae383ed037b12f13c437fdef83a12419ca789 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:06:22 -0400 Subject: [PATCH 44/73] fix(taskroute): the routing fixtures' example plan documents get ordinary names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ripwirepubliccheck arm 8 forbids a tracked file pointing at an internal-pattern .md name (PLAN_x.md, DESIGN_x.md) that does not ship. Lane R's new plan-lint rows and one source comment used such names as EXAMPLES. The router keys on the .md path token and the 'plan' wording, not on the prefix, so docs/next-plan.md routes exactly as docs/PLAN_NEXT.md did — taskroutecheck stays ALL PASS. --- src/taskroute.h | 2 +- test/taskroutecheck.sh | 10 +++++----- test/taskroutefix/PROVENANCE.md | 2 +- test/taskroutefix/prompts.tsv | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/taskroute.h b/src/taskroute.h index 92ac33216..f8b06d295 100644 --- a/src/taskroute.h +++ b/src/taskroute.h @@ -737,7 +737,7 @@ inline std::optional catalogTaskChoice( std::string_view task, std: // there, so the route fires only when the task names one. const std::string planDoc = firstPathTokenWithSuffix( task, { ".md", ".markdown" } ); const std::string planDocL = lowerAscii( planDoc ); - // The file may name ITSELF as the plan (PLAN_CACHE.md, DESIGN_NOTES.md) — that is surface evidence + // The file may name ITSELF as the plan (docs/cache-plan.md, docs/design-notes.md) — that is surface evidence // the task's prose does not have to repeat. if( ( has( lower, "plan" ) || has( lower, "design doc" ) || has( lower, "design document" ) || has( planDocL, "plan" ) || has( planDocL, "design" ) ) diff --git a/test/taskroutecheck.sh b/test/taskroutecheck.sh index 63672eb25..0f004d6d1 100755 --- a/test/taskroutecheck.sh +++ b/test/taskroutecheck.sh @@ -233,8 +233,8 @@ HO="$( route 'I am going on leave next week - put together a brief on the schedu case "$HO" in *'status="recommend"'*'intent="handoff-brief"'*'skill="ripwire-handoff"'*'--handoff'*) ok "briefing a second party -> --handoff";; *) no "handoff route wrong: $HO";; esac HO0="$( route 'we handed the account off to support last week, any update on the customer?' )" case "$HO0" in *'--handoff'*) no "an account handover minted a --handoff route: $HO0";; *) ok "prose about handing over anything else mints no --handoff";; esac -PL="$( route 'check that docs/PLAN_NEXT.md is well-formed as a plan document' )" -case "$PL" in *'status="recommend"'*'intent="plan-lint"'*'--plan-lint='*'docs/PLAN_NEXT.md'*) ok "plan-structure wording + a named markdown file -> --plan-lint=FILE";; *) no "plan-lint route wrong: $PL";; esac +PL="$( route 'check that docs/next-plan.md is well-formed as a plan document' )" +case "$PL" in *'status="recommend"'*'intent="plan-lint"'*'--plan-lint='*'docs/next-plan.md'*) ok "plan-structure wording + a named markdown file -> --plan-lint=FILE";; *) no "plan-lint route wrong: $PL";; esac # Value-carrying, like --edit-plan: the verb refuses a file that is not there, so no file, no command. PL0="$( route 'can you lint the structure of our planning docs in general?' )" case "$PL0" in *'--plan-lint='*) no "plan-lint invented a file the task never named: $PL0";; *) ok "plan-lint abstains rather than invent a plan document";; esac @@ -269,11 +269,11 @@ GQRUN="$( "$BIN" "$REPO" --no-cache --graph-query="$GQEXPR" )"; rc=$? { [ $rc -eq 0 ] && printf '%s' "$GQRUN" | grep -q '"$REPO/PLAN_GATE.md" -PLRUN="$( "$BIN" "$REPO" --no-cache --plan-lint=PLAN_GATE.md )"; rc=$? +printf '# A plan\n\n## Goal\n\nship it\n' >"$REPO/plan-gate.md" +PLRUN="$( "$BIN" "$REPO" --no-cache --plan-lint=plan-gate.md )"; rc=$? [ $rc -le 2 ] && ok "the emitted --plan-lint=FILE command runs against a real plan file (rc=$rc)" \ || no "the emitted --plan-lint=FILE command failed to run (rc=$rc)" -rm -f "$REPO/PLAN_GATE.md" +rm -f "$REPO/plan-gate.md" # ── two routers, ONE vocabulary: every shipped skill must be nameable by --help-task ────────────────── # F-R1-09 measured 8 of 16. This arm reads BOTH sides from disk — the skill directories that exist, and # the skill= names src/taskroute.h can emit — so it fails when a NEW skill ships with no route as much as diff --git a/test/taskroutefix/PROVENANCE.md b/test/taskroutefix/PROVENANCE.md index 6eddcb519..fc1d13794 100644 --- a/test/taskroutefix/PROVENANCE.md +++ b/test/taskroutefix/PROVENANCE.md @@ -257,7 +257,7 @@ inherited a support queue, a profiler vendor selling licences, a quarterly summa **Two corrections the pre-insertion verification caught, recorded rather than smoothed over:** -- *"lint the shape of DESIGN_NOTES.md before I circulate it"* abstained: the surface test wanted the +- *"lint the shape of docs/design-notes.md before I circulate it"* abstained: the surface test wanted the words "plan"/"design doc" in the PROSE. A file that names ITSELF a plan (`PLAN_*.md`, `DESIGN_*.md`) is surface evidence the prose need not repeat, so the check now reads the named file's own name too. - *"lint the plan file layout before I commit it"* routed to `quality-check`. `"before i commit"` is a diff --git a/test/taskroutefix/prompts.tsv b/test/taskroutefix/prompts.tsv index fc4f1fff1..9fb17af2f 100644 --- a/test/taskroutefix/prompts.tsv +++ b/test/taskroutefix/prompts.tsv @@ -191,9 +191,9 @@ test clean understand-symbol instrumented-cli how does classify work? show me th dev clean handoff-brief instrumented-cli I am going on leave next week - put together a brief on the scheduler for whoever takes it over test clean handoff-brief instrumented-cli hand this area off to the next session with the entry points and the risky bits test clean handoff-brief instrumented-cli my teammate is picking this up tomorrow, write the handover for the ingest path -test clean plan-lint instrumented-cli does PLAN_CACHE_ROUND.md have the structure a plan file is supposed to have? -dev clean plan-lint instrumented-cli lint the shape of DESIGN_NOTES.md before I circulate it -test clean plan-lint instrumented-cli check that docs/PLAN_NEXT.md is well-formed as a plan document +test clean plan-lint instrumented-cli does docs/cache-round-plan.md have the structure a plan file is supposed to have? +dev clean plan-lint instrumented-cli lint the shape of docs/design-notes.md before I circulate it +test clean plan-lint instrumented-cli check that docs/next-plan.md is well-formed as a plan document dev clean trace-prose instrumented-cli I have a sanitizer report from last night - map it onto the indexed symbols test clean trace-prose instrumented-cli here is a compiler error from the build, which of my symbols does it name test clean trace-prose instrumented-cli got a crash log with frames in it, translate it into symbols I can read From 434b2fd61755e3346f4667e4e5791993d2ca09b2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:06:22 -0400 Subject: [PATCH 45/73] docs(didyoumean): the edit-distance cutoff's comment loses its audit-round coordinate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lane H's widened cap register renders kMaxEditDistance's trailing comment into docs/LIMITS.md, where ripwirepubliccheck arm 3 refuses a §P12.1 coordinate in a shipped doc. Same sentence, no coordinate; LIMITS.md regenerated. --- docs/LIMITS.md | 2 +- src/didyoumean.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index ab2701d2f..3777ebbbf 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -200,7 +200,7 @@ Discloses: **none** | constant | value | class | note | | --- | --- | --- | --- | -| `kMaxEditDistance` | `3` | BOUNDARY | bandwidth cutoff (§P12.1): beyond this a "hint" is noise, not help | +| `kMaxEditDistance` | `3` | BOUNDARY | bandwidth cutoff: beyond this edit distance a "hint" is noise, not help | | `kMaxEditDistance` | `3` | BOUNDARY | same bandwidth cutoff as didYouMean | ### `src/dmm.h` diff --git a/src/didyoumean.h b/src/didyoumean.h index 0411f03e0..3794492d8 100644 --- a/src/didyoumean.h +++ b/src/didyoumean.h @@ -202,7 +202,7 @@ inline std::string_view nearestSymbolNameWhere( const IngestResult& ing, std::st { return {}; // F10: the nearest name to "" is the shortest name, which is a suggestion about nothing } - constexpr int kMaxEditDistance = 3; // bandwidth cutoff (§P12.1): beyond this a "hint" is noise, not help + constexpr int kMaxEditDistance = 3; // bandwidth cutoff: beyond this edit distance a "hint" is noise, not help return nearestNameByEditDistance( ing.symbols.begin(), ing.symbols.end(), typed, kMaxEditDistance, [ keep ]( const Symbol& s ) -> std::string_view { return keep( s ) ? std::string_view( s.name ) : std::string_view(); } ); From 8309660784983935edd487bc8802bedc7312848d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:06:22 -0400 Subject: [PATCH 46/73] test(mcp): the manifest ceiling re-anchors at 42,200 with the two lanes' bytes attributed Lanes R (no_route on for/explore, +254 B) and H2 (limit/offset on flags and situational_awareness, +610 B) each re-anchored alone at 41,650 and 42,000; the merged tree measures 42,084 B against main's 41,220 B. Attributed tool by tool against main's binary in the gate header; headroom 116 B, less than one declared argument (rule 5). --- test/mcpmanifestcheck.sh | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/test/mcpmanifestcheck.sh b/test/mcpmanifestcheck.sh index a7fcd4bb8..1664d4874 100755 --- a/test/mcpmanifestcheck.sh +++ b/test/mcpmanifestcheck.sh @@ -192,7 +192,17 @@ tools = json.loads( line )[ "result" ][ "tools" ] # TOTAL 40,986 -> 40,902 B; nothing else moved. Raw wire bytes (this gate measures json.dumps # with ensure_ascii, which spends 6 for each em dash instead of 3): 40,901 -> 40,811. # Headroom goes back UP, 14 B -> 98 B. That is item 5 below working, not a new allowance. -CEILING = 42000 +# RE-ANCHORED 2026-09-10 (the string/perf round's integration, two lanes each declaring arguments): +# for, explore +127 B each = +254 B: `no_route` (mirrors the CLI --no-route so an MCP agent that reads +# route= and disagrees has a recovery path — R1 finding F-R1-07) +# flags +329 B (+145 B description, `limit`/`offset` properties): the dark-flag site listing and +# the six --flip listings join the paging family and disclose their cuts (C1 F-07) +# situational_awareness +281 B (+97 B description, `limit`/`offset`): --situ's blast-radius and co-change +# listings page instead of cutting silently at 8 (C1 F-10) +# TOTAL 41,220 -> 42,084 B on the merged tree, attributed tool by tool against main's binary +# (both lanes had re-anchored alone — 41,650 and 42,000 — and the sum is what ships). +# Headroom after this line: 116 B, less than one declared argument, which is rule 5 above working. +CEILING = 42200 manifest = len( json.dumps( { "tools": tools }, separators = ( ",", ":" ) ) ) descBytes = sum( len( t[ "description" ] ) for t in tools ) schemaBytes = sum( len( json.dumps( t[ "inputSchema" ], separators = ( ",", ":" ) ) ) for t in tools ) From 1ec420f1f2ff84f28bbfc58079499839030d2397 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:06:22 -0400 Subject: [PATCH 47/73] docs(lineage): the string kernels' sources, credited where the technique actually lives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Langdale & Lemire (VLDB J. 2019) for the nibble-table byte classification classMasks is built on; Daniel Lemire's 2023-07-13 blog code for the working SSE classifier and the SWAR case fold the fused subtoken hash uses; StringZilla for the NEON movemask, the 256-bit byteset test and the SWAR has-zero-byte probe behind findByteset and the escapers; Tempesta fast_str for the branchless A-Z fold and the 32-byte classification step. Each row names the function in src/infra/strkern.h it lands in. Sources scanned but whose technique is not in the shipped code (Muła's MPSADBW search, simdutf) get no row, per the owner. Counts re-derived: 49 repositories and 70 papers folded (readmedriftcheck E1-E10 green); the deck's long form updated in the generator (present/deck5_ripwire_build.js); the .pptx is regenerated by 'node deck5_ripwire_build.js' at release time, per present/README.md. --- README.md | 4 ++-- docs/LINEAGE.md | 8 ++++++-- present/deck5_ripwire_build.js | 4 ++-- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 3ccda16ce..f90131acd 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ what it breaks, which tests to run — instead of grepping around and reading wh
-Fifty years of software-engineering results, and research from last month. 46 repositories and 69 papers folded — McCabe (1976) through to seven published in the last two months — each row in docs/LINEAGE.md naming the lesson taken and the file it lives in +Fifty years of software-engineering results, and research from last month. 49 repositories and 70 papers folded — McCabe (1976) through to seven published in the last two months — each row in docs/LINEAGE.md naming the lesson taken and the file it lives in Beside those sits a labelled survey of **237 tools** that contributed nothing and says so. The two sets are disjoint by construction, so they add rather than nest — a tool that gave a lesson is never @@ -1672,7 +1672,7 @@ timing-only, and `pmccheck`'s inactive arm now proves that was truly the case. 43 repositories, 69 papers and a 237-tool survey — and the study where search over a pre-built index beats a delegating planner 65.2% to 46.2%, at under half the cost Almost none of the ideas here are new; the combination and the constraints are. Lessons folded from -**46 repositories and 69 papers** into one deterministic executable, alongside a labelled +**49 repositories and 70 papers** into one deterministic executable, alongside a labelled survey of 237 tools that folded nothing and are catalogued separately — the two sets are disjoint, so they add rather than nest. The row-by-row ledger, each with the lesson taken and where it lives, is [`docs/LINEAGE.md`](docs/LINEAGE.md). Those three counts are derived from that document's own tables diff --git a/docs/LINEAGE.md b/docs/LINEAGE.md index 88bff17d1..7515ecab3 100644 --- a/docs/LINEAGE.md +++ b/docs/LINEAGE.md @@ -21,10 +21,10 @@ licence in [`THIRD_PARTY.md`](../THIRD_PARTY.md). First-party code under `src/` third-party code lives under `third_party/` and keeps its own licence. Citing a paper means the idea was read and applied, not that any of its text or code is here. -**The counts, derived from the tables below:** **46 repositories** and **69 papers** are folded, and +**The counts, derived from the tables below:** **49 repositories** and **70 papers** are folded, and a labelled survey of **237 tools** contributed nothing and says so. **The two sets are disjoint by construction, so they add rather than nest:** a tool that contributed a lesson gets a row in §3a and -is never repeated in §3b, which makes the field study 46 folded *plus* 237 surveyed — not 46 picked +is never repeated in §3b, which makes the field study 49 folded *plus* 237 surveyed — not 49 picked out of 237. `test/readmedriftcheck.sh` re-derives all three numbers from these tables on every run, fails if the README's sentence disagrees, and proves the disjointness itself (arm E6) rather than taking this paragraph's word for it. Arm E9 checks this second restatement of the pair independently @@ -124,6 +124,7 @@ practitioner article. Each is labelled as such in its own row rather than left t | Bogliolo, *From SQL Generation to Tool Selection: A Domain-Oriented Pattern for MCP Servers* — [arXiv:2608.22063](https://arxiv.org/abs/2608.22063) | A fixed, domain-aligned tool set beats free-form query synthesis and lets small models match large ones on it — the paper's own pooled score moved 0.605 (generic tool pack) to 0.939 (verticalized pack), with the smallest model tested improving most. External corroboration, after the fact, of a closed verb catalogue over an open query language. | The closed verb catalogue in `--help`; `--graph-query=EXPR`'s closed node-set expression language (`src/verbs_navigate.h`); `--verify=CLAIM`'s closed claim grammar with a three-valued verdict (`src/verify.h`) | | Paul, Helm, Glavaš & Gurevych, *OctoLong: Mid-Training on Cross-Repository Code Contexts Enhances Long-Context Modeling* — [arXiv:2608.05141](https://arxiv.org/abs/2608.05141) | A cross-repository context-curation pipeline is only as trustworthy as its linking discipline — OctoLong instruments an AST parser, a language server and a package manager to curate genuinely dependency-linked spans across repository boundaries rather than merely co-located ones. External corroboration of the same discipline applied here. | Multi-root workspaces (`ripwire ...`): cross-root edges admitted only on explicit include/import/FFI evidence, never on same-name coincidence (`src/resolve.h`) | | Shao, Zeng, Zhao & Yu, *HyperFL: Query-Adaptive Representation Learning for Software Fault Localization* — [arXiv:2608.02967](https://arxiv.org/abs/2608.02967) | Adapting the retrieval representation to the shape of the query, rather than serving every query from one fixed embedding space, is worth doing — HyperFL does it with a learned hypernetwork generating per-query LoRA parameters. External motivation, after the fact and without the learner, for choosing a representation deterministically instead. | The `--for` query-shape router: name-exact BM25 versus subtoken+body BM25, chosen per query with the evidence printed as `route=` (`src/lexical.h`, `src/filter.h`) | +| Langdale & Lemire, *Parsing Gigabytes of JSON per Second* — [doi:10.1007/s00778-019-00578-5](https://doi.org/10.1007/s00778-019-00578-5) (VLDB Journal 2019) | Classify every byte of a block with two 16-entry shuffles — the low nibble indexes a column bitmap, the high nibble selects a row bit — and then reason about the text as bitmasks rather than bytes. | `classMasks` in `src/infra/strkern.h`, and the tokenizer that consumes its masks (`src/lexindex.h`: token starts and camel/acronym splits are mask algebra, not a per-byte state machine) | **Two co-change parameters here were derived independently and landed on the published values — and saying so is stronger than silence.** The bulk-commit cap (`kCoBoostMaxFilesPerCommit`, `src/gitmine.h`) @@ -246,6 +247,9 @@ shipped target links, and it is named in the near-miss paragraph below rather th | [tgrep](https://github.com/microsoft/tgrep) | Two lessons, and the first is the one an agent feels: **a search tool must not make you learn a second grep.** tgrep enforces ripgrep parity with a 2,530-line parity suite and a 5,768-line compatibility suite and documents every remaining divergence by name. Measured against that standard, `--regex`'s `^` and `$` were FILE anchors in a verb whose every answer is a line — `--regex='^#include'` returned 1 hit over `src/` where `rg` returned 1,648. The obvious repair, `std::regex::multiline`, faults inside libc++ (it reads one byte before the buffer at offset 0; 8 of 10 runs crashed, and twenty plain-build gates passed on the crashing binary where one ASan run would have named it), so the scanner now searches one LINE at a time, which is what grep, rg and tgrep do; post-fix parity with `rg` is exact on five anchored patterns. Second lesson, from tgrep's 64 MiB size cap: **state what a cut COST, not only that one happened** — tgrep prices its cap in the same paragraph as its benefit, where `--grep` printed `complete="1"` over a corpus whose built-in denylist had silently removed `third_party/` (33 hits served against `rg`'s 78 on this repository). Taken from the 2026-09-09 head-to-head (`bench/tgrep-h2h/`, five rungs from 159 to 182,555 files), which also priced the RESIDENT form of the Zoekt row above: the crossover Q* is 9.5 queries against `rg` at 2,240 files and 2.5 at 182,555 against a measured median of 26 grep-class calls per session — decisive — and yet neither half of tgrep's margin is available here: the larger half is a resident file-CONTENT cache a one-shot CLI cannot have (G5; on a query whose plan narrows nothing, the server still beats `rg` 15×), and the portable half, posting lists in the warm cache, is **refuted by measurement**, because `startGrepScanPrefetch` already runs the scan concurrent with the graph build and the ingest wins that race at every rung. What that measurement pointed at instead was ripwire's own: a warm-ingest floor that was super-linear (40 µs/file at 15,865 files, 938 µs/file at 182,555) — answered the same day by the profiling lane as the CHA-lite cone rebuilt per call and memoised (`ChaConeMemo`, `src/graph.h`; warm llvm `--grep` 159.7 s → 9.2 s), which moves this row's Q\* against ripwire-warm at the top rung from 0.1 to ≈1.7. | `src/search.h` (`grepScanText` line-at-a-time, `kGrepRegexSyntax`), `src/verbs_grep.h` + `src/mcpverbs.h` (`corpus_pruned_dirs=` on the `` root, CLI and MCP), gate `test/grepanchorcheck.sh` (arm I is the crash regression); the harness and crossover table in `bench/tgrep-h2h/` and [`EVALS.md`](EVALS.md) | | [codeburn](https://github.com/getagentseal/codeburn) | Measure the thing you claim to optimise, in the unit you claim it in, and mark every place the number is an estimate rather than a reading. codeburn prices 41 agents' token usage from their own logs, carries a per-call `costIsEstimated` flag end-to-end through its cache, and surfaces an unpriced model rather than rendering `$0` as free; its own defect is the same shape it exposed here — the flag reaches three of its ~eight rendering surfaces. ripwire's `est_tokens` had the identical gap: gated for determinism and monotonicity, never for accuracy, and absent on 16 of 25 verbs including every navigation verb. Folded as the missing instrument, not as code: `est_tokens` against a real tokenizer per verb (median +15.9% / +18.5% over-read, range −18.4% to +41.7%, driven by document SHAPE — rows at 2.4 bytes per token, legend prose at 4.7 — not by the corpus language the rate is keyed on), the legend's share in TOKENS beside the byte claim `--help` publishes (`--impact`'s ≥50% saving holds in bytes and is 39.4% in tokens), and a whole-loop ledger over 78 local transcripts: ripwire answers are 42% of retrieval calls and 4.3% of retrieval tokens, and cache reads are 98% of everything billed, so an answer is re-paid once per remaining turn and a per-call price sees only the first payment. The negative lesson is load-bearing: codeburn prints COST, and cost is the wrong unit here — its price table is an unpinned network fetch and a number that goes stale in the artifact — so `est_tokens` stays the unit and no cost surface was built. | `bench/tokenaudit/` (`sweep.py`, `pin.py`, `loop_ledger.py`); the calibration band `test/tokenbudgetcheck.sh` #18 over the frozen `test/estcalibfix/` and `test/estcalib.manifest` (no tokenizer at gate time); the corrected §H7 sentence in `src/serialize.h`; [`EVALS.md`](EVALS.md) "`est_tokens` against a real tokenizer, and the loop the per-call number cannot see" | | [markitdown](https://github.com/microsoft/markitdown) | Its README states the privilege model of a converter that shells out — it runs "with the privileges of the current process" — and tells callers to use the narrowest entry point. Applied to ripwire's OWN shell-outs rather than to the markitdown bridge it already carries: a PATH shim logged 82 read-only `git` calls across a 13-verb sweep, all inside the analysed checkout, where a hook-form `core.fsmonitor` in that checkout's `.git/config` is a command git runs on every one of them — a fixture hook fired three times per `--situ`. Neutralised at one site before any thread, disclosed on stderr and in `--doctor`, git's builtin boolean daemon left alone, and a tiered local-config pre-scan keeps the warm map at 52.7 ms where one git config probe per run cost 9.9 ms of it. Surveyed and NOT folded for the plain-text prose tier that landed beside it: markitdown has no `.rst`/`.adoc`/`.org` converter either, and its `.txt` acceptance is the generic catch-all a three-corpus census refuted (14–23% prose) — `.rst/.adoc/.org/.mdx` joined the markdown grammar tier on ripwire's own measurement (rst title underlines are setext; heading-tiled served 15/15 against 9/15 for one whole-file unit on the astropy docs), not on markitdown's. Turning its bridge on for binary formats was REFUTED with a design of what would have to be true first: the extractor's version in the cache key, `prov=` on the row, a named `extractor_missing=` instead of the silent zero the live bridge produces today, and opt-in. | `src/githarden.h`, `src/main.cpp` (one call after `parseArgs`), `src/verbs_doctor.h` (`git-config-trust`), `SECURITY.md`, gate `test/githardencheck.sh`; the prose tier's one vocabulary in `src/docparse.h` and its grammar rows in `src/ingest_crawl.h`, gate `test/textdocscheck.sh` | +| [Daniel Lemire's blog code, 2023-07-13](https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog/tree/master/2023/07/13/src) | The nibble-table classifier as working SSE code, and the SWAR case fold that lowercases eight bytes with high-bit arithmetic and no per-byte branch. | `classMasks` (the two-stage lookup tables) and `swarLowerFold8` in `src/infra/strkern.h`; the fused subtoken hash in `src/lexindex.h` folds its bytes with it | +| [StringZilla](https://github.com/ashvardanian/StringZilla) | On NEON there is no `pmovmskb`: `vshrn_n_u16( …, 4 )` plus one 64-bit lane read is the movemask; a 256-bit byte set is tested sixteen bytes at a time as two table lookups over `(byte >> 3, byte & 7)`; the portable path is the exact SWAR has-zero-byte probe. | `neonNibbleMask`/`neonByteMask`, `Byteset256` + `findByteset`, `swarZeroByteMask` in `src/infra/strkern.h`; the XML and JSON escapers (`src/serialize.h`, `src/infra/jsonesc.h`) copy clean runs between the bytes the set finds | +| [Tempesta FW `fast_str`](https://github.com/tempesta-tech/blog/tree/master/fast_str) | Lowercase A–Z branchlessly as one wrapping subtract and one unsigned compare, with the 0x80-bias spelling when the ISA only compares signed bytes; classify with the same two-stage shuffle at 32 bytes per step. | `lowerFoldAscii` / `lowerFoldedEquals` and the AVX2 path of `classMasks` in `src/infra/strkern.h` | **Read and not folded, and worth naming because they are the near misses.** A scoped-snippet view with scope breadcrumbs — the one rung of the detail ladder that is still missing here — was designed diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 364ae1fb6..ac06ee9c5 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -923,7 +923,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// standing on giants", AMBER); title(s, "The research inside — classic and current"); s.addText([ - { text: "46 repositories + 69 papers folded", options: { color: TEXT, bold: true } }, + { text: "49 repositories + 70 papers folded", options: { color: TEXT, bold: true } }, { text: " · and a labelled survey of 237 tools that contributed nothing, which says so — every row with the lesson taken and where it lives: docs/LINEAGE.md", options: { color: MUTED } }, ], { x: MX, y: 1.58, w: 12.0, h: 0.34, fontFace: SANS, fontSize: 13, margin: 0 }); const classics = [ @@ -972,7 +972,7 @@ function row(s, y, h, cols, opts={}){ ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], ["597 gate scripts", "bash test/manifestcheck.sh"], // gatecount - ["46 repos · 69 papers · 237 surveyed","bash test/readmedriftcheck.sh"], + ["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/"], ["the oracle round", "bench/headtohead/r9-2026-08-09/RESULTS.md"], From 06414b5265394c28d6fa671552831659b6d1db14 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:20:20 -0400 Subject: [PATCH 48/73] fix(mention): three helpers the disclosure rewrite left with no caller are gone MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit namesUnkeptPackageIndex, mentionFilesCut and namesFileNotKept lost their last callers when the file-cap verdict became 'total > kept.size()' (lane H); the wave-level --quality-delta's dead-code kind — which lane Q taught to see header symbols — reported them. Deleted, not acked. --- .ripwire_quality_acks | 13 +++++++++++++ src/mention.h | 40 ++++------------------------------------ 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 68d08668e..e733f1466 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -86,6 +86,7 @@ ack api-surface 7c0e356e60b323ba 3 cid=4451e903bc990da2 M13 paging/budget parity ack api-surface 7c2c696cc3c55bd4 8 cid=fafc1666106ab470 E1 seam rules (terminality round A): applyEdit gains the SeamInfo out-param (7->8) so every surface can disclose trailing_newline_folded/separator_padded; the 7-arg wrapper was removed rather than kept as a duplicate ack api-surface 7ed8ad2c213537a4 7 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). ack api-surface 7f2c3eefdf6e512e 3 R-R root-relative emission lane: threading the corpus root into 8 emitters is the change itself — +1 param each (contract-change), +3 cx from each pathRel lambda, and the verbosity of the relativization plus its comments. Reviewed row by row; none is avoidable without abandoning root-relative emission. Storage keys unmoved (baseline + ack ledger byte-identical across the cure). +ack api-surface 7f5e07a10dd97563 4 cid=1cc3fb32d2789e8a P2-2 regex hoist: passesPredicates takes the per-predicate compiled-regex table as one explicit parameter, every caller updated in the same commit (lane F; --lint/--match byte-identical) ack api-surface 80b08c75913ae76c 8 cid=2b1373e91d63396e by=src/* lane/n6-d, the registered offset-table retry of docs/EVALS.md 'The auto-cache key ignores --exclude' (bands (6)-(8)). All seven gating rows are this lane's own footprint on the two cache seams; the three rows that were REAL are FIXED rather than acked (below). (1) api-surface contract-change loadCache 4->5 and runParsePool 7->8. loadCache's old fourth parameter was 'long long& blobWriteNsOut'; it is replaced by the crawled-file list plus a CacheLoadStats out-struct, because the whole point of v15 is that a load deserialises ONLY the records for the files THIS crawl asked for, and a load that is not told the crawl cannot do that. runParsePool takes that same struct through so the RIPWIRE_CACHE_STATS line can report cached_records=/blob_entries= — the two numbers that make band (2) an executable fact instead of a wall-clock claim (test/cacheoffsetcheck.sh check (e)). Both are internal to ingest.cpp's single TU, one call site each, updated in the same commit; no consumer outside the TU ever saw either signature. (2) five short-horizon-churn churn=self rows on kCacheVersion, kIngestCacheVersionMirror, loadCache, saveCache and runParsePool: the footprint of editing exactly the symbols a format bump must edit, in a window that also holds the gate commit. Not thrash — a version constant and its gated mirror must move together in one commit by construction (qextractionkeycheck). WHAT WAS FIXED INSTEAD OF ACKED, because it was real: saveCache's complexity 94->125 and verbosity 285->408 are gone (zero regression) after the seven per-file fact-grouping loops moved to buildCacheFileIndexes, the path/order prologue to buildCachePathKeys, and the plan/carry/trailer work to buildCacheWritePlan/appendCarryRecord/finishCacheBlob; and the duplication row against ingest_sidecap.h TreeGuard::operator= is gone because ReadFd dropped its move-assignment for an openOnce() that fills an empty guard, the only mutation the type needs. Verification at this head: test/cacheoffsetcheck.sh ALL PASS (written RED first at 8411f7e), the whole cache family green, ASan+UBSan+LSan clean on cold store, warm load, subset load and carry-over save on both the fixture and this repo, three-run byte determinism, warm==--no-cache, xmllint clean. ack api-surface 81fbe59b4a35659b 11 cid=c5e9778e250e41f1 capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement | prior: WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack api-surface 84b6bfc164c989e8 2 cid=87c39ba5f968fb34 M21(a) sa sym=/p=: staleAcksXml takes the caller's XML escaper as a template parameter (+1 param) because sym= carries a canonical id — corpus text — and quality.h sits BELOW serialize.h in the include order. testmap.h's runHint uses the same seam for the same reason; the alternative was including serialize.h from quality.h, which inverts the order. @@ -354,6 +355,7 @@ ack complexity 00864cc4801c708a 18 cid=88b8c4067d691df7 timsort vendoring: every ack complexity 05b5f1acf3a7e880 263 cid=ddad1d0b7ec13821 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack complexity 060a064b6ffa7775 243 cid=605cbb1f768828e0 P2.2 register-macro dead-code fix: additive params on computeDelta/isDeadCandidate, complexity/verbosity growth in computeDelta and runQualityViews (the --dead-code verb), and the kQSnapCacheScheme bump line sit inside the in-window churn threshold - all eight gating rows are this lane's own footprint, none foreign ack complexity 0a3d16d6f3139408 29 cid=e2b9873df9888866 by=src/* answer-safe --edit-check window: the three contract-change rows are ONE defaulted paging pair (pageLimit/pageOffset, 0/0 = the verb's own default cap) plumbed through the ONE assembler and its two front doors — editCheckBundleText, editpreview::run, editCheckText — rather than a second capped emitter, because two emitters would drift and a page that drifted could drop the flagged caller that IS the answer. The complexity/verbosity growth in the assembler is the partition-preserving row loop (the window advances on UNFLAGGED rows only, so a flagged row and its sites_l= ride every page uncut) plus the in-band legend that says what pages and what never does; the churn rows are this lane's own footprint across cli.h/mcp*/editcheck*, none foreign. | prior: round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut +ack complexity 0f6e0499ab027efe 31 cid=0af5efd0a6e96865 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack complexity 170085af31722733 17 E1 answer grader + questions task source + claude-runner control isolation (F3). churn=self on run_one/main/build_prompt/_claude_metrics/_harness_metrics/_execute/prepare_codex_environment/prepare_opencode_environment is this one change's own edit window on bench/agentloop/run_agentloop.py, which the opencode round touched days ago: the three preparers now share ephemeral_run_home()+link_credential() and the three command builders share build_harness_command(), so every one of those symbols is edited by the same commit that introduces the claude preparer. run_one's complexity regression was FIXED rather than acked (25->under the bar, by extracting build_harness_command/question_timeout/prepare_environment) and the two dead-code rows were fixed by restoring explicit dispatch — a dict of callables had hidden the codex/opencode preparers from the resolver. The remaining duplication row (prepare_claude_environment | prepare_codex_environment, 101 tokens, down from 282) is DELIBERATE and not further factored: the residue is 'ephemeral home named by one env var + credentials symlinked + set the var', and collapsing it would need a nine-parameter helper that opencode still could not use (it derives every path from xdg-basedir at module load, so five dirs plus HOME must move). Each preparer is asserted independently by its own canary gate — agentloopclaudecheck/agentloopopencodecheck/agentloopcodexcheck — and keeping the three recipes separately readable is the point of those gates. grade_answers.py's new-symbol rows are the six protocol grader types plus the closed accept-rule clause grammar; apply_clauses is a flat per-clause-kind dispatch with no nesting, and transcript_answer_text's error-masking row is the deliberate 'schema drift degrades to nulls, never raises' contract the retained transcript makes safe. Gates: agentloopgradercheck (new, born red on origin/main at exit 2) and agentloopclaudecheck (new, born red at exit 1) both green; codex/opencode canaries and analyze.py --self-test unchanged and green. ack complexity 19d43d944ddcd186 44 cid=533f2648b2fac227 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack complexity 19e15f944de795a8 44 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. @@ -486,6 +488,7 @@ ack duplication 1096a473dedb29cf 128 taskroute v1 landing: (1) the five clone/ne ack duplication 1104136ee19ceb5c 58 octocode F3: matchNearestKindClause follows the established purpose-named optional-clause-builder idiom already used twice in this tree (mcprefusal.h::blankPayloadClause, selectorrefuse.h::selectorFaultClause) — the guard-empty/build-prefix/conditional-suffix shape recurs by that idiom's own design, not by accidental copy-paste; sharing a generic builder with AnchorList::clause (an unrelated --for routing internal) would couple two unrelated subsystems for a shape-only match ack duplication 12a98f49622cc5c4 39 main.cpp split 2026-08-29: NOT new duplication — the 39-token AnchorKey::operator< / model.h lessUnindexedExt near-miss pair existed verbatim at the base (operator< moved byte-identical from main.cpp to verbs_report.h); clone groups key on their member set, so the moved path minted a new group id for a pre-split pair. argvdiffcheck proves the bodies unchanged ack duplication 160ac41979d9ebaf 674 capture-audit 2026-09-04 wave-1 close: symbols two lanes each grew past the other's acked magnitude — runDoctor (L10 legend + blobs_floor=, L9 built_from=), writeEnsembleReport (L9 root=, L10 conditional unavailable=), runAffected/runVerify (L9 root-relative block, L4 gauge), writeTestGateReport (L4 gauge splice, L9 row-gated root=; the XML/JSON twins' duplication is the lockstep mcpclidiffcheck asserts, as L9 acked; graphGaugeAttrXml/Json are the same lockstep), writePanelReport (L10 conditional attrs, L4 counts_floor), printUsage/validateConfig (L1 H10 hoist + L5/L9/L10 help text). Re-acked at the merged magnitude; prior reasons kept | prior: M12 (lane L9, capture-audit-2026-09-04): the deliberate cost of one root-relative path spelling across --affected/--test-gate/edit receipts/fetch_body plus the in_id= legend trim. runAffected grows the same mvSingleRoot/mvRootPrefix/mvRootAttr block verbs_report.h's dispatcher already threads (complexity 13->18, verbosity +17, mostly the comment naming the finding); writeTestGateReport/Json's duplication is the XML/JSON twin pair staying in lockstep, which is the property mcpclidiffcheck asserts; every short-horizon-churn row is this lane editing its own targets three times in one afternoon. +ack duplication 189c716a1f2298cc 42 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication 197e1f32ffe97006 42 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 199088adf212a996 95 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. ack duplication 19b0ff8a7ad07b3c 63 F4: withHeaderField and withHeaderAttrAfter now share headerFieldDigits; the 63 tokens left are the four-line frame both need (copy the line, locate, edit, return). Folding those into one function with a mode flag would be worse than the clone. @@ -499,6 +502,7 @@ ack duplication 296b8816e4cc2ce6 40 P3.2 plan-lint mechanical footprint: printUs ack duplication 2d090ba54aef3c26 589 WAVE-2 close (2026-08-19), finding 2 of 3 triaged separately. duplication emitCochangeGroups | emitCochangePairs (589 tokens, src/main.cpp:4317). These two cochange emitters were already structurally parallel before the wave; W2-E's root-relative p= landing added the SAME four-line preamble to each (xxSingleRoot / xxRootPrefix / xxRootAttr from rw::sarif, plus the rootRelPathsLegend call and the root=-before-at= placement), which is what carried the pair over the clone bar. The shared parts that COULD be hoisted already were: rootRelPathsLegend/kRootRelPathsLegend are one definition serving all eighteen legends (the S B4 echo-site rule, see ack api-surface 48efeb1d). What remains per-site is three locals computed from each emitter's OWN cfg/ing bindings in its own scope; folding those into a helper would hand every emitter a struct it must unpack, at ~30 call sites, to save three lines each. Accepted as the cost of the uniform root= contract, NOT as a claim that the preamble is optimal: the ~30-site preamble echo is recorded as a wave-2 follow-up for a dedicated round, where it can be measured and gated instead of refactored blind at integration close. ack duplication 33c5d907ab904cbe 43 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 340861e6b3c185e4 25 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. +ack duplication 36e29c8cfeef798b 361 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication 3999cec4b0ce2903 18 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication 3a1a9d56db5c56ce 49 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 duplication 3caf742bad9a9545 56 --quality-panel: the fixed structural cost of adding a verb. printUsage/honorsPaging churn+verbosity is the same one-line flag-table edit every paginated verb makes (the --help block is already trimmed to 10 lines and is the authority for a six-family verb with three presets); the three ensemble.h helpers were touched ONCE to hoist them out of a real duplication finding, which is the fix, not the debt; parsePreset x skilleval::parseProv is an incidental shape clone - two table-lookup loops with nothing to share but their shape. @@ -554,6 +558,7 @@ ack duplication b16454c12b64a72e 52 cachefix fixture functions deliberately mirr ack duplication b3ba17bfe8eaffde 38 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 b7f09332215eedbc 70 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 duplication b93c23254ec1e5bf 77 by=src/* rung 3 flow-sensitive reaching definitions (docs/EVALS.md 'Flow-sensitive slice in the small', 2026-09-03, lane/n6-b). edgesOf: the contract change IS the point — the diff now reads the scan's reach table (scan, rowOfOcc, cap) so rows/flow/diff share one edge oracle. sliceEmitBody +4 cx: the rd= emission per use row (its formatting already factored into sliceAppendReachAttr). The three duplication rows are the idiomatic tree-sitter named-child loop (seq/hasStructureBelow vs ingest helpers) and a two-way family ternary — no shared logic to lift. short-horizon-churn = self-churn of the two files this lane owns. sliceLegendText +11 lines = the registered per-construct disclosures the band requires on the first screen. +ack duplication b94f262fa5cff312 361 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication bbf5277c0bf46e5f 29 check() is the house standalone-harness helper (each test harness deliberately self-contained); pmccheck_harness added the 5th member ack duplication bd9c7867dc3d87a7 95 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. ack duplication bdb1e60b2f6b343a 64 wave merge 2026-08-20 (harvestexec): isTypeMentionNode, from lane/resolver-precision. Both duplication rows pair it with an EXISTING member of src/ingest.cpp's kind-table family (isBaseTypeNode, isJsonShapeModifier) — a static const char* const table with one commented row per language, scanned by strcmp. What the clone detector sees is that shared idiom, and it is right that it is shared: it is the file's house pattern for a node-kind predicate, and every future language row lands as one commented line rather than an edit to a condition. Neither collapse is available. Merging into isBaseTypeNode would be a CORRECTNESS change, not a cleanup: base-type admits eight kinds (qualified_identifier, generic_type, user_type...) and a type MENTION must admit only type_identifier — that narrowness is precisely the round-1 ACCEPT result, and test/typerefcheck.sh fails if it widens. Collapsing today's single-row table to a bare strcmp would remove the per-language comment slot the siblings use and would have to be undone by the first non-C++ grammar added. Acked as a deliberate instance of an established shape, not as a shortcut. @@ -571,7 +576,9 @@ ack duplication d3df68780c6d8f90 26 cache-pack round, reviewed each: fixture fun ack duplication d77ef6ec7d956c38 46 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 d842417635ec5627 38 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 duplication daefdef824b8a96b 40 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. +ack duplication dc5ec483298d7df6 214 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication ddce9efe392269e6 61 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. +ack duplication de772ce9497e6cc2 425 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication dec50094e5aef506 74 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack duplication df5a70dc16c85001 33 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication e0406bb747448d85 154 ingest() decomposition: emitBindingAliases/emitRouteDefs are the FFI-alias and route-def sort ladders moved verbatim out of ingest()'s body into named phase helpers (ingest_model.h); as inline statements the clone scanner could not see them, as small functions their comparator shape token-matches sortLintRows - different element types and orderings, no shareable contract, zero behavior change (argvdiff byte-identical) @@ -585,6 +592,7 @@ ack duplication ec3f7b5523723637 110 C1 F-06/F-07/F-10 (the listing-paging round ack duplication ef3d5272b705b505 107 ingest.cpp split 2026-08-29: pre-split clone pairs (buildNewlineOffsets vs the bench_newline_ab arms, acked lane-B3 at keys 98099b517e9d2fbb/a6f7de52f80c9f48) whose clone-group keys changed because buildNewlineOffsets moved VERBATIM into ingest_astquery.h — the disclosed clone-ack rename floor, same artifact as the main.cpp split's moved-clone row; argvdiffcheck vs c267a4b proves no body changed ack duplication f14cfce02be351ad 24 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication f3518ef93569f6ae 25 idiom-class clone false positive: a three-way ternary over string literals, 25 normalized tokens, sharing no domain identifier with macroRoleAttr and in an unrelated subsystem. Reading both confirms it. +ack duplication f4f3fc233cdda6ba 307 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack duplication f5a96185fd557a83 61 R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. ack duplication fb9ac4034d49c58e 50 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack duplication ff14ff2b821304c8 24 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) @@ -600,6 +608,7 @@ ack new-clone-of-reused-helper 0e9742973611fd71 3 T3 disclosure-gap fix 2026-08- ack new-clone-of-reused-helper 1096a473dedb29cf 4 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 new-clone-of-reused-helper 1cd9e6b6ec0829c1 3 R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. ack new-clone-of-reused-helper 340861e6b3c185e4 3 Two shapes, both read and both kept. (1) docDriftText/strayContentText/flipText are three-line index-plus-captureXml front doors that were already near-identical; adding a page argument moved two of them across the 95-token bar. Folding them costs a wrapper that hides which compute call each makes, on the exact seam an MCP reader needs to see. (2) budgetBytesForTokens vs ceilingAllowanceBytes is a SHAPE match, not a duplicate: one spends kBudgetHeadroom (the shaping budget), the other kCeilingFirstEntryTolerance (the overshoot bar), they are used in opposite directions, and serialize.h states the distinction where both are read. Merging them would be the real bug. +ack new-clone-of-reused-helper 36e29c8cfeef798b 120 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper 3999cec4b0ce2903 4 by=src/* slice honesty round (lane H): token-shape clones between one-line predicate/name-table helpers in slice.h and unrelated domains (naminglens predicatePrefixed, pattern servedNames/resolvedNames, quality headSnapExclHex, model localsCountedLang) — no shared identifiers, different scopes; reviewed by the lane and the orchestrator and declined to merge across domains (the clone-idiom class) ack new-clone-of-reused-helper 3a1a9d56db5c56ce 6 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 new-clone-of-reused-helper 48c8751d3adcc20c 4 F9: emptyvaluerefusecheck's mcp_reply is the one-shot JSON-RPC caller every MCP-touching gate spells for itself. Gate scripts are standalone by contract (binoverridecheck/gateexitcheck/pargates treat any top-level test/*.sh as a gate), and test/lib/ holds exactly one sourced helper by deliberate precedent; a second one is a round's decision, not this lane's. @@ -621,13 +630,16 @@ ack new-clone-of-reused-helper 9497791b092d0735 4 M10 (capture-audit L9): at= an ack new-clone-of-reused-helper 9b15c6e24f597b61 4 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 new-clone-of-reused-helper a5943aaae0184b8f 4 readability-wave1 (naming lens): ncAnyOf is a one-expression std::find wrapper, sortNotes a one-expression std::stable_sort wrapper — 30 normalized tokens of the SAME STL-adapter idiom over unrelated types (a char-class set test vs a note sort). There is no call ncAnyOf could make to sortNotes, and rewriting the std::find as a hand loop would only hide the detector. Kept as written. ack new-clone-of-reused-helper aada023eba533a38 4 lane C plain-text prose tier (test/textdocscheck.sh): .rst/.adoc/.org/.mdx join kLangTable on Lang::Markdown so --recall can answer from an ADR that is not written in markdown. All five gating rows are this lane's own footprint. TWO short-horizon-churn churn=self rows: kLangTable is the language table this change exists to extend, and kParserVer is the cache key an extraction change is REQUIRED to move (ingest_cache.h's own note says so) — both are structural for any lane of this kind, not thrash. THREE clone rows on isMarkdownGrammarExtension, all 35-token idiom collisions on a one-line membership predicate: it now spells the sorted-table + std::binary_search + is_sorted static_assert shape that externalnames.h::isShellBuiltinName/isPythonBuiltin/isCFamilyStdName already carry (their own note at externalnames.h:98 records this exact collision and settles on this shape), and the KindCounts::total pair is a std::accumulate over std::begin/std::end normalizing to the same token stream. Two cheaper spellings were tried and REJECTED by measurement first: a hand-rolled scan loop is the five-instance clone shape ingest.h::isNonTextExtension's note already names, and a std::find one-liner cloned KindCounts::total alone. What was FIXED rather than acked in this pass: six duplication rows (the loop -> the house binary_search shape) and kLangTable's verbosity row 96->120 (the tiling essay moved out of the table body onto the seam above it). +ack new-clone-of-reused-helper b94f262fa5cff312 120 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper c085689a09c879ee 3 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. ack new-clone-of-reused-helper c0e6e599da682c8a 3 idiom-class clone: a NAMED one-line std::find predicate. What matches is the std::find idiom itself — 36 normalized tokens, zero shared domain identifiers with ncAnyOf/namesNode, different value types, different subsystems. Inlining it at its two call sites to dodge the row would be metric-gaming; the name is the documentation. ack new-clone-of-reused-helper c8b32435256b863e 4 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 new-clone-of-reused-helper cf6c83caaec68565 10 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 new-clone-of-reused-helper d11c67611bc73d49 3 mcpgrepdegradedcheck round (2026-08-30): the gate's mcp_text/batch_sub/call shell harness is the 7th deliberate copy of mcpclidiffcheck's — gate scripts are self-contained by design (each runs standalone under RIPWIRE_BIN with no shared lib to source), the same trade every earlier MCP gate made; a shared test/lib refactor is its own round, not this diff's ack new-clone-of-reused-helper daefdef824b8a96b 3 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. +ack new-clone-of-reused-helper dc5ec483298d7df6 16 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper ddce9efe392269e6 4 2026-08-19 subtoken acronym round: lexSubtokenHash now lowercases EVERY byte (an all-caps run is one token, so interior uppercase is reachable), which turned its head-special-case into a plain absorb loop and made it a 61-token type-3 match for the two other FNV loops in the tree (mcpindex byteHash, clones cloneSketchMatches). Different basis constants and different semantics; unifying them is a separate round, and reshaping correct code purely to miss the clone detector would be gaming the number. +ack new-clone-of-reused-helper de772ce9497e6cc2 7 test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) ack new-clone-of-reused-helper dec50094e5aef506 3 OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack new-clone-of-reused-helper e6ae1365cb6c154a 3 V3 harvest 2026-08-15: two MCP gate harnesses. Every MCP gate in this suite is deliberately STANDALONE — the house rule in their own headers is 'does NOT edit regression.sh or any other existing test file', so a gate carries its own 3-4 line JSON-RPC transport wrapper (mcp_call in mcphandlecheck, mcpCall in mcpeditpresencecheck, the curl wrapper in mcpremotecheck). test/mcptoolprunecheck.sh needs BOTH transports (HTTP for the pinned-root arms A-E/G, stdio for arm F, and the pair is the point: pinning is exactly what makes the omission provable), so its http_call and stdio_call land as the 9th and 10th members of two families that already exist. Extracting a shared test/lib harness would couple every MCP gate to one file and is a suite-wide refactor, not this lane's; sharing one of the two existing spellings instead would make this gate fail whenever an unrelated gate edits its own helper. No production code involved ack new-clone-of-reused-helper f5a96185fd557a83 3 R2 pattern lane: the six gating rows left after two real extractions are idiom and dispatcher footprint, not new debt. runLint complexity/verbosity: adding a FIFTH verb to a five-verb dispatcher grows it; the compile, refusal and disclosure assembly are already out in runPatternSearch (the split runMatchQuery established) and the legend is out in kPatternLegend, so what remains is the emission loop every sibling verb also has inline. The four lintCatalogFind clone rows: a std::find_if over a constexpr table returning pointer-or-null IS the idiomatic spelling, and wrapping std::find_if to make two three-line lookups tokenize differently is the 'do not game the number' case this verb's own legend names. @@ -640,6 +652,7 @@ ack params 1085f731a3dde7c8 7 cid=b012ca29106914d1 capture-audit 2026-09-04 wave ack params 1520fa02411735c3 6 cid=69e2cb4c55a88771 C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack params 19d43d944ddcd186 6 cid=533f2648b2fac227 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack params 19e15f944de795a8 6 cid=e1412db291c8eaa4 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. +ack params 28c51bbd0032f9ec 9 cid=bdd33965f128de92 bench/capsweep write_screen renders the nine screen.tsv columns capsweepcheck pins by name; a record object would move the schema the gate reads ack params 3561d0281d324276 13 V1 harvest 2026-08-15: packBodies +8 cx/+20 LOC is the withFileContext branch + fileCtx table build/lookup for octocode F2's sibs=/inc=; the attribute-building itself was extracted to appendFileExpandContextAttrs (mirroring the pre-existing emitCalleeCallsBlock split) to keep this at the minimum needed to wire the new opt-in path ack params 3c07d993bfdbce53 9 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack params 453ce415b663d773 6 cid=27e74116f23adc21 timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. diff --git a/src/mention.h b/src/mention.h index ab9c6abe2..12c95c840 100644 --- a/src/mention.h +++ b/src/mention.h @@ -149,7 +149,7 @@ inline void noteCap( InfoT* outInfo, const char* cappedAttr, const char* totalAt // kMentionMaxDirectSymbols 0/39. All three firings came from the same shape — a task that pastes SEVERAL // paths, which is exactly the multi-file localization case B8 exists for. The kMentionMaxFiles 3/39 is an // UPPER BOUND: it was read off the first cut of the flag, which reported a scan's STOP as a cut on exactly -// that multi-path shape (see namesFileNotKept), and the 39-task list was not kept, so it cannot be re-derived. The zero is a property of this +// that multi-path shape (see mentionUnkeptFiles), and the 39-task list was not kept, so it cannot be re-derived. The zero is a property of this // corpus, not of the cap: it takes more than eight definitions of one Scope.name for the cap to bite, and // a C++ tree with unique method names has none. The class is real and gated on a fixture that does // (test/mentioncapcheck.sh arm C), and the attribute costs nothing on the runs where it stays silent, so @@ -394,21 +394,8 @@ inline bool definesScopeName( const IngestResult& ing, const std::string& scope, return std::any_of( ing.symbols.begin(), ing.symbols.end(), [ & ]( const Symbol& s ) { return s.name == name && s.scope == scope; } ); } -inline bool namesUnkeptPackageIndex( const IngestResult& ing, const RawMention& m, const std::vector& kept ) -{ - for( std::uint32_t f = 0; f < ing.files.size(); ++f ) - { - if( isIndexBaseName( baseNameOf( ing.files[f] ) ) && dirSuffixMatches( ing.files[f], m.segments ) - && std::find( kept.begin(), kept.end(), f ) == kept.end() ) - { - return true; - } - } - return false; -} - // WHICH files this mention names that `kept` does not — appended, never cleared, so a caller can union -// across mentions. This is namesFileNotKept's body with "return true on the first one" replaced by "collect +// across mentions. This is the old first-hit predicate's body with "return true on the first one" replaced by "collect // them all": a bare boolean told the caller something was withheld and neither how much nor how to get it, // which is §9-3 of docs/METHODOLOGY.md unmet, and the sibling caps in this same file already pass a total. // Verdict-equivalent to the predicate below by construction — same resolution order, same three rules, and @@ -454,27 +441,8 @@ inline void mentionUnkeptFiles( const IngestResult& ing, const RawMention& m, co } } -inline bool namesFileNotKept( const IngestResult& ing, const RawMention& m, const std::vector& kept ) -{ - std::vector unkept; - mentionUnkeptFiles( ing, m, kept, unkept ); - return !unkept.empty(); -} - -// The file-cap verdict for the whole task: did kMentionMaxFiles keep out a file ANY mention names? A cut needs a full -// list, so the common anchored query pays one size test. A mention resolved while the list still had room kept -// everything it named and answers false, so asking every mention is exact — no bookkeeping of which one filled it. -inline bool mentionFilesCut( const IngestResult& ing, const std::vector& raw, const std::vector& kept ) -{ - if( kept.size() < kMentionMaxFiles ) - { - return false; - } - return std::any_of( raw.begin(), raw.end(), [ & ]( const RawMention& m ) { return namesFileNotKept( ing, m, kept ); } ); -} - // How many DISTINCT files the task's mentions name in total — the kept ones plus the ones the cap refused. -// Equal to kept.size() when nothing was cut, so `total > kept.size()` is exactly mentionFilesCut's verdict +// Equal to kept.size() when nothing was cut, so `total > kept.size()` is exactly the file-cap verdict // and the two can never disagree. The union is only computed on the runs where the list is full, so the // common anchored query pays the same one size test it always did. inline std::uint32_t mentionFilesNamedTotal( const IngestResult& ing, const std::vector& raw, @@ -701,7 +669,7 @@ inline bool applyMentionBoost( const IngestResult& ing, std::string_view task, s liftPackageDirMention( ing, m, mentionedFiles ); } } - // a STOP is not a CUT (namesFileNotKept), and a CUT without a total is a fact the caller cannot act on: + // a STOP is not a CUT (mentionUnkeptFiles), and a CUT without a total is a fact the caller cannot act on: // mention_files_total= is every distinct file the task names, so `total - shown` is what the cap withheld. const std::uint32_t mentionFilesTotal = mentionFilesNamedTotal( ing, raw, mentionedFiles ); noteCap( outInfo, "mention_files_capped", "mention_files_total", From 17058123e06fd439e24ba8bac2d7a0a376e71daf Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 21:23:31 -0400 Subject: [PATCH 49/73] test(emittertruth): the api-new-surface= count gets its zero probe (Z2g) and its roster line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Lane Q's --quality-delta legend now carries a second 'printed even at zero' claim in verbs_quality.h (the api-new-surface= count that replaced the never-gating new-symbol rows). The gate's roster caught the new claim; (Z2g) proves it on a body-only change in the zero repo — XML carries api-new-surface="0", the JSON dialect carries the key — and the roster expects 2. --- test/emittertruthcheck.sh | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/emittertruthcheck.sh b/test/emittertruthcheck.sh index e1c0467d5..ac1a73b87 100755 --- a/test/emittertruthcheck.sh +++ b/test/emittertruthcheck.sh @@ -420,7 +420,7 @@ ingest_astquery.h|never suppressed|1 landingplan.h|always printed|1 verbs_grep.h|always emitted|1 verbs_grep.h|never suppressed|1 -verbs_quality.h|printed even at zero|1 +verbs_quality.h|printed even at zero|2 verbs_report.h|never omitted|1 EOF )" @@ -532,6 +532,26 @@ else || no "(Z2c) --quality-delta --json DROPPED register-macro-excluded" fi +# ── (Z2g) api-new-surface= — 'printed even at zero' (verbs_quality.h, the second claim in that file) ──── +# The zero repo's change added an export (api-new-surface=1), so make a second change that adds NONE: the +# body of keep() moves, no new symbol. The count must still ride, as the legend promises, at 0. +( + cd "$ZQ" && git add lib.h && git commit -qm added + printf 'int keep( int a ) { return a + 3; }\nint added( int a ) { return a + 2; }\n' > lib.h +) >/dev/null 2>&1 +"$BIN" "$ZQ" --quality-delta --no-cache > "$TMP/zqd2.xml" 2>/dev/null +"$BIN" "$ZQ" --quality-delta --json --no-cache > "$TMP/zqd2.json" 2>/dev/null +if ! grep -q ' "$TMP/zco.xml" 2>/dev/null zCoRoot="$( grep -o ']*>' "$TMP/zco.xml" | head -1 )" From 7d31a5a113235524071fbf99f7307ce6a1b05399 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:00:32 -0400 Subject: [PATCH 50/73] =?UTF-8?q?fix(lexindex):=20the=20AVX2=20block=20mas?= =?UTF-8?q?k's=20seam=20bit=20is=20masked=20before=20the=20shift=20?= =?UTF-8?q?=E2=80=94=20the=20ubuntu=20ASan=20abort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -fsanitize=integer's unsigned-shift-base check aborted at lexindex.h:186 on x86-64: the 32-byte AVX2 block fills every bit of the uint32 mask, and `m.alnum << 1` drops its top bit — the block's last byte, which is exactly the seam carry prevAlnum/prevUpper already hand to the next block. On NEON the block is 16 bytes and the top half of the mask is always zero, which is why every arm64 ASan run (five lanes) was clean. The bit is masked off before the shift; output byte-identical on ripwire and go (map, --for, --pack-task). strkerncheck gains arm 3b: the x86_64 Rosetta slice compiled with -fsanitize=undefined,integer — RED on the unfixed source with CI's exact line, green after. Arm 3 (no sanitizers) was the coverage gap that let this reach CI. --- src/lexindex.h | 9 +++++++-- test/strkerncheck.sh | 24 ++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/lexindex.h b/src/lexindex.h index 17739f413..9aaa404cf 100644 --- a/src/lexindex.h +++ b/src/lexindex.h @@ -183,8 +183,13 @@ inline void forEachLexTokenSpan( std::string_view text, EmitSpanFn&& emitSpan ) const unsigned char after = ( base + width < n ) ? static_cast( p[ base + width ] ) : 0u; const std::uint32_t nextLowerBit = ( after >= 'a' && after <= 'z' ) ? ( std::uint32_t( 1 ) << ( width - 1 ) ) : 0u; - const std::uint32_t alnumShift = ( m.alnum << 1 ) | ( prevAlnum ? 1u : 0u ); // bit k = A[k-1] - const std::uint32_t upperShift = ( m.upper << 1 ) | ( prevUpper ? 1u : 0u ); // bit k = U[k-1] + // The block's LAST bit is the next block's carry (prevAlnum/prevUpper below), so it is dropped from + // the shift on purpose — masked out first, because on the 32-byte AVX2 block every bit of the mask + // is live and `x << 1` losing a set bit is what -fsanitize=integer's unsigned-shift-base rejects + // (a 16-byte NEON mask never had a bit there to lose, which is why arm64 never saw it). + constexpr std::uint32_t kBelowTop = ~std::uint32_t( 0 ) >> 1; + const std::uint32_t alnumShift = ( ( m.alnum & kBelowTop ) << 1 ) | ( prevAlnum ? 1u : 0u ); // bit k = A[k-1] + const std::uint32_t upperShift = ( ( m.upper & kBelowTop ) << 1 ) | ( prevUpper ? 1u : 0u ); // bit k = U[k-1] const std::uint32_t lowerAhead = ( m.lower >> 1 ) | nextLowerBit; // bit k = L[k+1] const std::uint32_t split = m.upper & alnumShift & ( ~upperShift | lowerAhead ) & valid; diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 16a72c843..d9d1d1e6a 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -180,6 +180,30 @@ if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then fi fi +# ── 3b: the x86_64 slice under UBSan's integer checks — the arm that CI's ubuntu ASan leg is ──────────── +# The 32-byte AVX2 block fills every bit of a uint32 mask, so a `<< 1` that is harmless on a 16-byte NEON +# mask (top half always zero) DROPS a set bit on AVX2, and -fsanitize=integer's unsigned-shift-base check +# aborts on exactly that (PR #127's first CI run: lexindex.h:186 on --for/--pack-task, clean on every arm64 +# ASan run). Arm 3 compiled without sanitizers and could not see it. UBSan's runtime is a universal dylib +# in the Apple toolchain, so the cross slice CAN carry -fsanitize=undefined,integer; ASan stays off here +# (arm 1 owns memory safety on the host ISA). A sanitizer report is a FAIL; a slice that will not run at +# all (no Rosetta 2) is a SKIP, as in arm 3. +if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then + if X86UB="$( compile_direct x86ub -arch x86_64 -march=x86-64-v3 -fsanitize=undefined,integer -fno-sanitize-recover=all )" && [ -n "$X86UB" ]; then + if RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; then + read_counts "$WORK/out_x86ub.log" + printf ' PASS x86_64/AVX2 mirror is clean under -fsanitize=undefined,integer (%s assertions)\n' "$ASSERTS" + elif grep -q 'runtime error' "$WORK/out_x86ub.log"; then + echo " FAIL x86_64/AVX2 mirror trips UBSan integer checks: $( grep -m1 'runtime error' "$WORK/out_x86ub.log" | sed 's|.*/src/|src/|' )" + fail=1 + else + printf ' SKIP x86_64 UBSan slice built but did not run here (no Rosetta 2): %s\n' "$( tail -1 "$WORK/out_x86ub.log" )" + fi + else + printf ' SKIP no x86_64 UBSan cross slice on this toolchain; CI ubuntu-24.04 asan is the proof\n' + fi +fi + if [ "$fail" = 0 ]; then echo "strkerncheck: PASS" else From 82110b28e530645684e289dcbe83183bbc3e64e0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:00:32 -0400 Subject: [PATCH 51/73] test: four gates reconciled with what the round changed, and the quality-delta legend trimmed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit w3fixlegendcheck: --situ section [2] no longer carries a 25-row cap (retired by the listing-paging lane: answer rows never page; listingpagingcheck arm D pins the retirement) — the arm now demands ALL rows and NO cut. cppqualcheck: --uses=readWholeFile 22 -> 23 (src/lexical.h adopted the documented reader on the BM25 scan path). donelegendcheck: lane Q's two legend sentences trimmed to one clause each (-476 B against the first cut; the api-new-surface= sentence keeps emittertruthcheck's roster phrase); clean/scope/refpair clear their ceilings again, qd_dirty re-anchored 3800 -> 3900 with the measurement in the gate (45 B of headroom left, less than before). infraportcheck: four comment lines in src/infra/{fieldid,strkern}.h named the host project; reworded. --- src/infra/fieldid.h | 2 +- src/infra/strkern.h | 6 +++--- src/verbs_quality.h | 10 +++------- test/cppqualcheck.sh | 10 ++++++---- test/donelegendcheck.sh | 8 +++++++- test/w3fixlegendcheck.sh | 14 +++++++++----- 6 files changed, 29 insertions(+), 21 deletions(-) diff --git a/src/infra/fieldid.h b/src/infra/fieldid.h index 19e1a692f..88d55f4c7 100644 --- a/src/infra/fieldid.h +++ b/src/infra/fieldid.h @@ -18,7 +18,7 @@ // A 1 ms `sample` of a cold `--no-cache` run over the go corpus (15,865 files), 18,963 busy leaf // samples: `strncmp` + its two dyld stubs are **3.44 %** of busy CPU, the `ts_node_child_by_field_name` // subtree is 4.70 %, and **93.6 % of that subtree is owned by one caller** — `cc_boolOp`, which -// `cc_walk` asks twice per AST node at the fall-through of its dispatch. The ripwire tree's own share +// `cc_walk` asks twice per AST node at the fall-through of its dispatch. The host tree's own share // is 2.56 % (it is markdown-heavy, so less of it is AST walk). // // WHAT IT REPLACES IT WITH. `fieldChild( n, NodeField::Name )` resolves the id ONCE PER GRAMMAR, at diff --git a/src/infra/strkern.h b/src/infra/strkern.h index f4479780c..10cbaec98 100644 --- a/src/infra/strkern.h +++ b/src/infra/strkern.h @@ -2,7 +2,7 @@ // strkern.h — THE byte-parallel string kernels. One header, three mirrored paths, no other home. // -// House rule (owner, 2026-09-10): every SIMD string kernel ripwire owns lives HERE, as inline functions +// House rule (owner, 2026-09-10): every SIMD string kernel the host project owns lives HERE, as inline functions // with the NEON, the AVX2 and the scalar/SWAR reference written side by side in one place, so a reader // can diff the three by eye and a reviewer can see immediately when one path drifted. No SIMD intrinsic // for string work exists outside this header. (The pre-existing vector code in fixedStr.h, radixSort.h, @@ -254,7 +254,7 @@ inline void classMasks( const char* p, std::size_t n, Masks& out ) noexcept // 2. lowerFoldAscii / lowerFoldedEquals — the A-Z-only fold // ═══════════════════════════════════════════════════════════════════════════════════════════════════ // -// A-Z ONLY, on purpose: this is the fold ripwire's lexical layer means by "lowercase" (lexindex.h +// A-Z ONLY, on purpose: this is the fold the host's lexical layer means by "lowercase" (lexindex.h // lexLowerByte), and it is the only one that is locale-free, byte-exact and reversible enough to hash // with. Bytes >= 0x80 are left alone — a UTF-8 continuation byte is not a letter to fold. // @@ -763,7 +763,7 @@ inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& // box load 24-33). Milliseconds for the whole trace, lower is better: // // corpus per-byte switch run-copy + scalar scan run-copy + findByteset -// ripwire 1.60-2.00 0.81-1.01 0.71-0.96 −15% vs scalar +// host tree 1.60-2.00 0.81-1.01 0.71-0.96 −15% vs scalar // go 6.85-7.62 4.57-4.85 4.74-5.08 +1% (median len 8: 82% of // calls never reach a block) // django 8.00-9.12 3.80-4.30 2.97-3.11 −23% vs scalar diff --git a/src/verbs_quality.h b/src/verbs_quality.h index c364bd46a..e2e118fe6 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -486,10 +486,7 @@ inline constexpr const char* kQdLegendCore = "preexisting by construction. preexisting-worse= and new-symbol= partition regressions=. stale= is a " "FOURTH axis, never gating and never counted in regressions=: rows in the .ripwire_quality_acks ledger " "whose target no longer applies. " - "api-new-surface= is a COUNT, not a finding: how many symbols this change adds to the PUBLIC surface. " - "Never gates, never counted in regressions=, printed even at zero. It used to be one row per new export, " - "which the legend itself said could never gate; a count says the same thing without asking a reader to " - "page past it, and nothing narrows what the CONTRACT-CHANGE rows below still report. " + "api-new-surface= COUNTS the new PUBLIC symbols (never gates, not in regressions=, printed even at zero). " "register-macro-excluded= is a FLOOR, not a finding: symbols this run excluded from the dead-code kind " "because their own definition is a registered self-registering test/benchmark macro call. Never gates, " "never counted in regressions=, printed even at zero (zero means none excluded, not that the check did " @@ -623,9 +620,8 @@ inline constexpr const char* kQdRowLegend = "the numeric kinds; p=\"path:line\" is the locator (root-relative; the first-sorting member for the " "clone kinds; omitted, never faked, when none resolves). churn= and surface= are per-kind " "classification facets (short-horizon-churn's self/ambient split; api-surface's new-symbol/" - "contract-change tier). BOTH churn facets are informational: what gates that kind is a symbol whose " - "edited lines were rewritten by 2 or more COMMITTED commits inside the window, the working edit never " - "counted, so churn=\"self\" alone reports that this edit touches hot content and stops there. " + "contract-change tier). churn= facets never gate alone: the kind gates only on 2+ COMMITTED in-window " + "rewrites of the edited lines. " "Every row the header's gating= counter counts also carries a gating attribute " "set to 1 — marked positively, never by the ABSENCE of sev or origin. "; diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index ed571e657..d341e2234 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -178,7 +178,9 @@ US="$( run . --uses=selectBaseline --no-cache )" || no "repo: --uses=selectBaseline expected 2 incl. mcpverbs.h, got '$( cnt "$US" )': $US" # 4 -> 5 when src/readability.h adopted docparse::detail::readWholeFile as the canonical whole-file read # (the feat/readability-lens round); 5 -> 6 when src/renamemine.h adopted the same one (feat/naming-calibration); -# 6 -> 7 when src/commentcoherence.h adopted the same one (feat/comment-coherence). +# 6 -> 7 when src/commentcoherence.h adopted the same one (feat/comment-coherence); +# 22 -> 23 when src/lexical.h adopted the same one (the 2026-09-10 string-perf round: lexicalScanText read +# every file through ifstream + ostringstream << rdbuf() + str(), two copies, on the BM25 scan path). # The literal counts REAL call sites, so it moves when a real call site is # added; what it pins is that the qualified `docparse::detail::` spelling still RESOLVES, which is the defect # this arm was written for. Bumping it is correct; changing it to a >= would retire the arm. @@ -211,9 +213,9 @@ US="$( run . --uses=selectBaseline --no-cache )" # 19 -> 22 2026-09-09 (harvest githarden): githarden.h's local-config pre-scan reads the `.git` gitdir FILE, the # gitdir's `commondir`, and each config candidate through the same canonical helper — three sites for one probe, # rather than a fourth fopen/fread of its own. -[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 22 ] \ - && ok "repo: --uses=readWholeFile count=22 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ - || no "repo: --uses=readWholeFile expected 22" +[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 23 ] \ + && ok "repo: --uses=readWholeFile count=23 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ + || no "repo: --uses=readWholeFile expected 23" [ "$( cnt "$( run . --callers=writeTally --no-cache )" )" = 1 ] \ && ok "repo: --callers=writeTally count=1 (was 0 — both template call sites are in writeDocDriftPage)" \ || no "repo: --callers=writeTally expected 1" diff --git a/test/donelegendcheck.sh b/test/donelegendcheck.sh index 5dca37e45..e7d35d518 100755 --- a/test/donelegendcheck.sh +++ b/test/donelegendcheck.sh @@ -155,7 +155,13 @@ EOF run_budget qd_clean 2300 8574 "$FX" --quality-delta printf '%s' "$DIRT" >> "$FX/src/base.cpp" -run_budget qd_dirty 3800 8574 "$FX" --quality-delta +# qd_dirty 3800 -> 3900 (2026-09-10, the string/perf round): the --quality-delta legend gained two facts a reader +# needs to act on a row — the api-new-surface= count (one sentence, +105 B in every form, "printed even at zero" +# is emittertruthcheck's roster phrase) and the churn facets' gating rule (one clause). Measured on this fixture +# against the pre-round binary: clean 2177 -> 2282, dirty 3642 -> 3855, scope 4916 -> 5129, refpair 4058 -> 4271. +# Three forms still clear their ceilings; the dirty form's 3800 had 158 B of headroom and the two sentences cost +# 213 there, so the ceiling moves by less than the growth (45 B of headroom left) — a ratchet, not an allowance. +run_budget qd_dirty 3900 8574 "$FX" --quality-delta run_budget qd_dirty_scope 5200 10512 "$FX" --quality-delta "--scope=src/*" run_budget sd_uses 3800 4112 "$FX" --safe-delete=classifyWidth run_budget sd_none 3800 4112 "$FX" --safe-delete=tangle diff --git a/test/w3fixlegendcheck.sh b/test/w3fixlegendcheck.sh index 0f845f291..3909f1266 100755 --- a/test/w3fixlegendcheck.sh +++ b/test/w3fixlegendcheck.sh @@ -295,7 +295,10 @@ ANCH="$( dcCount "$ROOT" ./src )"; BARE="$( dcCount "$ROOT" src )" # ══ 4. --situ H6 disclosures + the M9 JSON twin ════════════════════════════════════════════════════════════ echo "── 4. situ cap disclosures + JSON twin" -# a sandbox where the test count EXCEEDS the 25-row cap (this repo's own probes stay under it). +# a sandbox where the test count EXCEEDS the old 25-row cap. That cap (kSituTestRowsShown) was RETIRED on +# 2026-09-10 (listing-paging lane): section [2] is the ANSWER of --situ — tests to run — and answer rows +# never page (docs/METHODOLOGY.md §9; test/listingpagingcheck.sh arm D pins the retirement red-first). So +# the disclosure this arm used to demand ("showing 25 of 30 tests") must now be ABSENT and every row listed. SITSB="$TMP/situsb"; mkdir -p "$SITSB/test" printf 'int coreFn(){ return 7; }\n' >"$SITSB/core.cpp" i=1; while [ $i -le 30 ]; do printf 'int coreFn();\nint t%02d_main(){ return coreFn(); }\n' "$i" >"$SITSB/test/t$i.cpp"; i=$(( i + 1 )); done @@ -303,11 +306,12 @@ i=1; while [ $i -le 30 ]; do printf 'int coreFn();\nint t%02d_main(){ return cor S2LINE="$( grep -E '^ \[2\]' "$TMP/situ30" || true )" S2ROWS="$( sed -n '/\[2\]/,/\[3\]/p' "$TMP/situ30" | grep -c 'test/t[0-9]*\.cpp' || true )" case "$S2LINE" in - *"(30)"*"showing 25 of 30 tests"*) ok "situ [2]: '(30) (showing 25 of 30 tests)' with ${S2ROWS} rows — the cap is disclosed";; - *) no "situ [2] does not disclose its 25-row cap: $S2LINE";; + *"showing "*" of "*) no "situ [2] still discloses a cut on its ANSWER rows (the 25-row cap was retired): $S2LINE";; + *"(30)"*) ok "situ [2]: '(30)' with no cut disclosed — answer rows never page";; + *) no "situ [2] header did not count all 30 tests: $S2LINE";; esac -[ "${S2ROWS:-0}" = 25 ] && ok "situ [2]: exactly 25 rows listed, matching the disclosure" \ - || no "situ [2] listed ${S2ROWS:-0} rows, disclosure says 25" +[ "${S2ROWS:-0}" = 30 ] && ok "situ [2]: all 30 rows listed (the retired 25-row cap is gone)" \ + || no "situ [2] listed ${S2ROWS:-0} rows, expected all 30 — answer rows never page" # section [3] on this repo (git history required for co-change partners). "$BIN" "$ROOT" --situ=src/graph.h >"$TMP/situ3" 2>&1 S3LINE="$( grep -E '^ \[3\]' "$TMP/situ3" || true )" From c8abcb199e1fb78b1e4c87d10ff125478bdba5a6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:06:22 -0400 Subject: [PATCH 52/73] fix(situ,fieldidcheck): the --situ follow-up hint echoes the locator-stripped selector; the field-id harness links LTO objects MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit situNextInvocation pasted the caller's own `--situ=F:1148` spelling into `next:`, so --situ=F:1148 and --situ=F differed by that one line and selectorchaincheck arm d2 went red on every shard 2/4. The hint now carries the selector after the same stripLineLocator normalization the verb itself applies — the canonical spelling, identical from either form. fieldidcheck's harness links the build's grammar objects directly; a Release build leaves them as LTO bitcode/GIMPLE that a plain Linux link cannot read (macOS's linker reads them transparently, which is why only the ubuntu Release legs were red). The gate links plainly first and retries the identical command with -flto. --- src/situ.h | 22 +++++++++++++++++++++- test/fieldidcheck.sh | 13 ++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/situ.h b/src/situ.h index 07c22c294..cd8be9475 100644 --- a/src/situ.h +++ b/src/situ.h @@ -395,7 +395,27 @@ struct SituPageArgs // it pastes back verbatim (bare --situ reads the git diff; --situ=F1,F2 named its own files). inline std::string situNextInvocation( std::string_view selector, std::size_t needed ) { - const std::string verb = selector.empty() ? std::string( "--situ" ) : ( "--situ=" + std::string( selector ) ); + // Echo the selector with each item's `:line` locator STRIPPED — the same normalization the verb applies + // before resolving (stripLineLocator, §P8 seam 2) — so `--situ=F:1148` and `--situ=F` produce byte-identical + // reports (selectorchaincheck arm d2) and the pasted follow-up is the canonical spelling, not the caller's. + std::string verb = "--situ"; + if( !selector.empty() ) + { + verb += "="; + std::size_t start = 0; + while( start <= selector.size() ) + { + const std::size_t comma = selector.find( ',', start ); + const std::string_view item = selector.substr( start, comma == std::string_view::npos ? std::string_view::npos : comma - start ); + verb += std::string( stripLineLocator( item ) ); + if( comma == std::string_view::npos ) + { + break; + } + verb += ","; + start = comma + 1; + } + } return verb + " --limit=" + std::to_string( needed ); } diff --git a/test/fieldidcheck.sh b/test/fieldidcheck.sh index cced1db33..eee0a0b67 100755 --- a/test/fieldidcheck.sh +++ b/test/fieldidcheck.sh @@ -360,7 +360,18 @@ int main( int argc, char** argv ) } """ ) PYHARNESS - "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra \ + # A Release build (RIPWIRE_LTO implied ON) leaves the grammar objects and libtree-sitter.a as LTO + # bitcode/GIMPLE, which a plain link cannot read on Linux ("plugin needed to handle lto object" / + # undefined tree_sitter_* references); macOS's linker reads them transparently, which is why this gate + # was green on every macOS leg and red on both ubuntu Release legs of PR #127's first run. Link plainly + # first (the plain build's objects), and retry the same command with -flto when that fails. + if "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra \ + -I "$incdir" -I "$ROOT/third_party/deps/tree_sitter/lib/include" \ + "$TMP/harness.cpp" $GRAMMAR_OBJS "$TSLIB" -o "$out" 2>"$log"; then + return 0 + fi + cp "$log" "$log.plain" + "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra -flto \ -I "$incdir" -I "$ROOT/third_party/deps/tree_sitter/lib/include" \ "$TMP/harness.cpp" $GRAMMAR_OBJS "$TSLIB" -o "$out" 2>"$log" } From f7498e736b822dc4f2e2fe01b8d026c2d53db604 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:17:21 -0400 Subject: [PATCH 53/73] fix(strkern): the trailing-zero count is std::countr_zero, not a GCC/Clang builtin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249663 (src/infra/strkern.h:436, +460/503/542/702) — VALID. The scalar twins findByte_scalar and find3_scalar are ALWAYS compiled and are the code that runs on a target with neither NEON nor AVX2; they used __builtin_ctzll, which MSVC does not provide. The project supports MSVC 19.36+ and the Windows port (PR #44) is pending, so the file would simply not have compiled there. The six vector sites go the same way for the same reason: MSVC compiles the AVX2 mirror under /arch:AVX2. All eight sites are now std::countr_zero( m ) with included. Same instruction on every toolchain that has one, and DEFINED at zero (returns the width) where the builtin is undefined — the change can only remove a footgun. Every site is already guarded by m != 0, so the value is unchanged at every one of them. BYTE-IDENTICAL, 12 of 12 proofs (build before vs after this commit, --no-cache): corpus --top-k=100000 --for=… --pack-task=… --grep=countr_zero ripwire 1,763,172 B same same same go 10,415,057 B same same same canyonraid48 7,229,007 B same same same GATE: test/strkerncheck.sh gains a SOURCE arm — 0 __builtin_ on a code line, >= 8 std::countr_zero( sites, included. It is a source arm on purpose: the only compiler on this box accepts both spellings, so no local build can tell them apart. CAN GO RED — observed firing ("uses 1 GCC/Clang-only __builtin_") before the arm was taught to skip comment lines. strkerncheck.sh: PASS — 19/19 assertions under full G1 sanitizers, NEON non-vacuity, -DSTRKERN_MUTATE=1 red as designed, Rosetta x86_64/AVX2 arms 3 and 3b both green. --- src/infra/strkern.h | 24 ++++++++++++++++-------- test/strkerncheck.sh | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/infra/strkern.h b/src/infra/strkern.h index 10cbaec98..5326ab7f1 100644 --- a/src/infra/strkern.h +++ b/src/infra/strkern.h @@ -46,11 +46,19 @@ // else — a cross build, a hand-configured toolchain that overrides the arch flags — compiles the scalar // twins, which are the same functions with the same contracts and no ISA requirement at all. // +// The scalar twins are ALWAYS compiled, so nothing in them may be a GCC/Clang extension: the trailing-zero +// count is `std::countr_zero` (, C++20) and not `__builtin_ctzll`, which MSVC — a supported compiler, +// and the one the pending Windows port builds with — does not provide. It is the same instruction on every +// toolchain that has one, and it is DEFINED at zero (width) where the builtin is undefined, so the change +// can only remove a footgun. The vector paths use the same spelling for the same reason: MSVC compiles them +// too under /arch:AVX2. +// // Determinism (docs/ARCHITECTURE.md §3): every kernel here is INTEGER and EXACT. Its result is a bit // pattern, not a rounded sum, so no path can reassociate its way to a different answer; the NEON, AVX2 // and scalar paths return identical values for identical input, which is precisely what // test/strkerncheck.sh asserts on 100k random buffers and on every byte of src/ and docs/. +#include // std::countr_zero — the portable spelling of ctz; the scalar twins must compile on MSVC too #include #include #include @@ -433,7 +441,7 @@ inline std::size_t findByte_scalar( const char* p, std::size_t n, char needle ) const std::uint64_t m = swarZeroByteMask( x ^ splat ); if( m != 0 ) { - return k + ( std::size_t( __builtin_ctzll( m ) ) >> 3 ); // the mask is EXACT: no verify pass + return k + ( std::size_t( std::countr_zero( m ) ) >> 3 ); // the mask is EXACT: no verify pass } } for( ; k < n; ++k ) @@ -457,7 +465,7 @@ inline std::size_t findByte( const char* p, std::size_t n, char needle ) noexcep const std::uint64_t m = neonNibbleMask( vceqq_u8( v, splat ) ); if( m != 0 ) { - return k + ( std::size_t( __builtin_ctzll( m ) ) >> 2 ); // four mask bits per input byte + return k + ( std::size_t( std::countr_zero( m ) ) >> 2 ); // four mask bits per input byte } } #elif defined( __AVX2__ ) @@ -468,7 +476,7 @@ inline std::size_t findByte( const char* p, std::size_t n, char needle ) noexcep const std::uint32_t m = std::uint32_t( _mm256_movemask_epi8( _mm256_cmpeq_epi8( v, splat ) ) ); if( m != 0 ) { - return k + std::size_t( __builtin_ctz( m ) ); + return k + std::size_t( std::countr_zero( m ) ); } } #endif @@ -500,7 +508,7 @@ inline std::size_t find3_scalar( const char* p, std::size_t n, const char* needl std::uint64_t m = swarZeroByteMask( a ^ n0 ) & swarZeroByteMask( b ^ n2 ); while( m != 0 ) { - const std::size_t at = k + ( std::size_t( __builtin_ctzll( m ) ) >> 3 ); + const std::size_t at = k + ( std::size_t( std::countr_zero( m ) ) >> 3 ); if( p[ at + 1 ] == needle[ 1 ] ) { return at; @@ -539,7 +547,7 @@ inline std::size_t find3( const char* p, std::size_t n, const char* needle ) noe // one NIBBLE per input byte, so the lowest set bit sits at 4 * byteIndex and clearing the // candidate means clearing its whole nibble — `m &= m - 1` (the bit-per-byte idiom) would // spin on the other three bits of the same byte. - const int lowBit = __builtin_ctzll( m ); + const int lowBit = std::countr_zero( m ); const std::size_t at = k + ( std::size_t( lowBit ) >> 2 ); if( p[ at + 1 ] == needle[ 1 ] ) { @@ -560,7 +568,7 @@ inline std::size_t find3( const char* p, std::size_t n, const char* needle ) noe _mm256_and_si256( _mm256_cmpeq_epi8( v0, s0 ), _mm256_cmpeq_epi8( v2, s2 ) ) ) ); while( m != 0 ) { - const std::size_t at = k + std::size_t( __builtin_ctz( m ) ); + const std::size_t at = k + std::size_t( std::countr_zero( m ) ); if( p[ at + 1 ] == needle[ 1 ] ) { return at; @@ -699,7 +707,7 @@ inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& const std::uint64_t m = neonNibbleMask( vtstq_u8( row, bit ) ); if( m != 0 ) { - return k + ( std::size_t( __builtin_ctzll( m ) ) >> 2 ); + return k + ( std::size_t( std::countr_zero( m ) ) >> 2 ); } } #elif defined( __AVX2__ ) @@ -725,7 +733,7 @@ inline std::size_t findByteset( const char* p, std::size_t n, const Byteset256& const std::uint32_t m = std::uint32_t( _mm256_movemask_epi8( hit ) ); if( m != 0 ) { - return k + std::size_t( __builtin_ctz( m ) ); + return k + std::size_t( std::countr_zero( m ) ); } } #endif diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index d9d1d1e6a..b7352597d 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -124,6 +124,38 @@ if [ -n "$WANT" ]; then fi fi +# ── COMPILER PORTABILITY, read off the SOURCE (CodeRabbit #127 / 3985249663) ───────────────────────── +# The scalar twins are ALWAYS compiled and the vector paths compile under MSVC's /arch:AVX2, so no path in +# this header may use a GCC/Clang-only builtin. `__builtin_ctzll` was the whole population: MSVC has no +# such intrinsic, and the pending Windows port (PR #44) would not have compiled the file at all. The +# portable spelling is 's std::countr_zero, which is the same instruction everywhere and is DEFINED +# at zero where the builtin is undefined. +# +# This is a SOURCE arm, not a build arm, and deliberately so: the only compiler on this box accepts both +# spellings, so no local build can tell them apart — the difference is visible in the text or nowhere. +# CAN GO RED: put `__builtin_ctzll` back on any one of the eight sites and this arm fires. +# CODE lines only: the prose above names the retired builtin on purpose, and a gate that cannot tell a +# comment from a call site would forbid writing down what the rule is. +HDR="$ROOT/src/infra/strkern.h" +code_hits(){ grep -n "$1" "$HDR" 2>/dev/null | grep -vE '^[0-9]+: *(//|\*|/\*)'; } +BUILTINS="$( code_hits '__builtin_' | wc -l | tr -d ' ' )" +CTZ="$( grep -c 'std::countr_zero(' "$HDR" 2>/dev/null || echo 0 )" +if [ "$BUILTINS" != "0" ]; then + echo " FAIL portability: src/infra/strkern.h uses $BUILTINS GCC/Clang-only __builtin_ — MSVC cannot compile it:" + code_hits '__builtin_' | sed 's/^/ /' | head -10 + fail=1 +elif [ "$CTZ" -lt 8 ]; then + echo " FAIL portability: only $CTZ std::countr_zero( call sites in strkern.h — the eight trailing-zero" + echo " counts (2 scalar twins + 6 vector) are the population this arm is non-vacuous over" + fail=1 +else + printf ' PASS portability: 0 __builtin_ in strkern.h, %s std::countr_zero( sites (MSVC-compilable; included)\n' "$CTZ" +fi +if ! grep -q '^#include ' "$HDR"; then + echo " FAIL portability: strkern.h calls std::countr_zero without including " + fail=1 +fi + # compile one flavour of the target directly; $1 = label, remaining args = extra compile flags. Echoes the # binary path on success, nothing on failure (the caller decides whether a compile failure is fatal). compile_direct() From dcbe3fc4e0c83b46260ef7f141b8eec68836a17c Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:21:53 -0400 Subject: [PATCH 54/73] test(strkern): every sweep probe runs on every iteration; only the message is first-wins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249745 (test/verify_strkern.cpp:612) — VALID. The 100k-iteration sweep drives every probe from ONE shared DeterministicRng, and five of the seven probes (probeClassMasks, probeFoldedEquals, probeFindByte, probeFind3, probeFindByteset) draw from it. `if( r.Fail.empty() )` therefore did not merely skip a report — it shortened the RNG stream, so every later buffer and every later probe input moved off the corpus the green run swept. A red run described an experiment nobody had ever seen pass, and an independent second divergence could be shifted out of existence by the first. Now: all seven probes run unconditionally; a `keep` lambda is the one place first-wins lives. `n > 0` stays on probeFoldedEquals — that is the probe's PRECONDITION, not a skip-on-failure. On a green run this is byte-for-byte the old behaviour (no arm ever holds a message, so every probe ran under the old spelling too, in the same order). GATE ARM 2b, test/strkerncheck.sh — "the failing sweep must have swept the same corpus". The harness prints `strkern sweep-rng: buffers=` (the generator's state after the loop — a pure function of the draw count, no extra draw), and the gate asserts the GREEN build and the -DSTRKERN_MUTATE=1 build print the SAME line. That is the property in one comparison, and it is what makes arm 2's red run evidence about the shipped kernels. CAN GO RED, observed: restoring the guard on probeClassMasks + probeFindByte gives green strkern sweep-rng: a00d1b843768cd12 buffers=100000 mutated strkern sweep-rng: 346abe780b440579 buffers=100000 and the arm fires. With the fix both read a00d1b843768cd12. strkerncheck.sh: PASS — 19/19 under full G1 sanitizers, NEON non-vacuity, portability, can-go-red 13/19 red as designed, sweep corpus, Rosetta arms 3 and 3b. --- test/strkerncheck.sh | 25 ++++++++++++++++++++ test/verify_strkern.cpp | 51 +++++++++++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index b7352597d..0f14cd09e 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -188,6 +188,31 @@ else printf ' PASS can-go-red: -DSTRKERN_MUTATE=1 fails %s of %s assertions as designed\n' "$ASSERTS_FAIL" "$ASSERTS" fi +# ── 2b: THE FAILING SWEEP MUST HAVE SWEPT THE SAME CORPUS (CodeRabbit #127 / 3985249745) ────────────── +# Arm 2's red run is only evidence about the SHIPPED kernels if the broken build walked the same buffers +# the green build walked. The sweep's probes draw from one shared DeterministicRng, so a probe skipped +# because its arm had already failed used to shorten the stream: every later buffer and every later probe +# input moved, and a second, independent divergence could be shifted out of the run entirely — the failure +# report then described a sweep nobody had ever seen green. verify_strkern.cpp now runs every probe +# unconditionally and keeps only the FIRST message per arm, which makes this comparison the proof. +# +# The line is `strkern sweep-rng: buffers=`; the state is the generator's, after the loop, so +# it is a pure function of how many draws were made. CAN GO RED: put the `if( r.Fail.empty() )` +# guards back and the mutated build — whose arms all fail on iteration 0 — prints a different state. +GREEN_RNG="$( grep -m1 '^strkern sweep-rng: ' "$WORK/out_main.log" 2>/dev/null )" +MUTATE_RNG="$( grep -m1 '^strkern sweep-rng: ' "$WORK/out_mutate.log" 2>/dev/null )" +if [ -z "$GREEN_RNG" ] || [ -z "$MUTATE_RNG" ]; then + echo " FAIL sweep corpus: no 'strkern sweep-rng:' line (green='$GREEN_RNG' mutated='$MUTATE_RNG')" + fail=1 +elif [ "$GREEN_RNG" = "$MUTATE_RNG" ]; then + printf ' PASS sweep corpus: the MUTATED build swept the same buffers as the green one (%s)\n' "$GREEN_RNG" +else + echo " FAIL sweep corpus: a failing arm moved the RNG stream — the red run is not the green run's sweep" + echo " green $GREEN_RNG" + echo " mutated $MUTATE_RNG" + fail=1 +fi + # ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── # The x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE; CMakeLists.txt sets it # unconditionally for x86-64 targets). Compiled without sanitizers — the ASan runtime for a cross-arch diff --git a/test/verify_strkern.cpp b/test/verify_strkern.cpp index 6f1f7f980..c415ef864 100644 --- a/test/verify_strkern.cpp +++ b/test/verify_strkern.cpp @@ -74,6 +74,7 @@ #include #include #include +#include // std::move — `keep` is the one place a first-wins message is retained #include #if !defined( RIPWIRE_TEST_ROOT ) @@ -437,8 +438,14 @@ const Sets& sets() struct Sweep { - std::size_t bufferCount = 0; - std::string classFail, foldFail, eqFail, findFail, tokFail; + std::size_t bufferCount = 0; + // The generator's state after the whole sweep — a pure function of HOW MANY draws the loop made, and + // therefore the fingerprint of the corpus every arm saw. It is printed and gated (strkerncheck.sh) + // because the sweep contract is that the corpus does NOT move when an arm fails: a build whose + // kernels are all broken must still have swept exactly the buffers the green build swept, or the + // failure it reports describes a different experiment. Pure observation — no extra draw. + std::uint64_t rngState = 0; + std::string classFail, foldFail, eqFail, findFail, tokFail; }; // ── one probe per kernel ────────────────────────────────────────────────────────────────────────────── @@ -602,25 +609,40 @@ const Sweep& sweep() drawBuffer( gen, alpha, n, buf ); ++r.bufferCount; - // Each probe is skipped once its arm has already failed — the arms report the FIRST - // divergence, and a kernel that is broken is broken 100k times over. - if( r.classFail.empty() ) { r.classFail = probeClassMasks( gen, buf, iter, alpha ); } - if( r.foldFail.empty() ) { r.foldFail = probeFold( buf, iter, alpha ); } - if( r.eqFail.empty() && n > 0 ) { r.eqFail = probeFoldedEquals( gen, buf, iter ); } - if( r.findFail.empty() ) { r.findFail = probeFindByte( gen, buf, iter ); } - if( r.findFail.empty() ) { r.findFail = probeFind3( gen, buf, iter ); } - if( r.findFail.empty() ) { r.findFail = probeFindByteset( gen, buf, iter ); } - if( r.tokFail.empty() ) + // EVERY probe runs on EVERY iteration; only the MESSAGE is first-wins. The arms report the + // first divergence, but the draw order is the contract: probeClassMasks, probeFoldedEquals, + // probeFindByte, probeFind3 and probeFindByteset each pull from `gen`, so skipping one after + // another arm had already failed moved every later buffer and every later probe input off the + // corpus the green run swept. The failing report then described a DIFFERENT sweep from the one + // that passed, and a second, independent divergence could be shifted out of existence by the + // first. `keep` is the one place first-wins lives (CodeRabbit #127 / 3985249745). + // + // On a GREEN run this is byte-for-byte the old behaviour: no arm ever holds a message, so every + // probe ran under the old spelling too, in this same order, off this same stream. + const auto keep = []( std::string& slot, std::string&& msg ) + { + if( slot.empty() ) { slot = std::move( msg ); } + }; + keep( r.classFail, probeClassMasks( gen, buf, iter, alpha ) ); + keep( r.foldFail, probeFold( buf, iter, alpha ) ); + if( n > 0 ) // a PRECONDITION of the probe, not a skip-on-failure + { + keep( r.eqFail, probeFoldedEquals( gen, buf, iter ) ); + } + keep( r.findFail, probeFindByte( gen, buf, iter ) ); + keep( r.findFail, probeFind3( gen, buf, iter ) ); + keep( r.findFail, probeFindByteset( gen, buf, iter ) ); { const std::string d = tokenizerDiff( buf ); if( !d.empty() ) { char msg[ 512 ]; std::snprintf( msg, sizeof( msg ), "iter=%d alpha=%d %s", iter, int( alpha ), d.c_str() ); - r.tokFail = msg; + keep( r.tokFail, msg ); } } } + r.rngState = gen.state; return r; }(); return s; @@ -1078,6 +1100,11 @@ TEST_CASE( "strkern: the compiled path is the one this target claims" ) // oracle to itself and prove nothing. std::printf( "strkern: path=%s block=%zu root=%s\n", sk::kPathName, sk::kBlockBytes, repoRoot() ); std::printf( "strkern path: %s\n", sk::kPathName ); + // The sweep's draw fingerprint, on its own grep-able line. strkerncheck.sh asserts the GREEN build and + // the -DSTRKERN_MUTATE=1 build print the SAME value: every probe runs on every iteration, so a failing + // arm cannot shorten the RNG stream and move the corpus out from under the arms that come after it. + std::printf( "strkern sweep-rng: %016llx buffers=%zu\n", + static_cast< unsigned long long >( sweep().rngState ), sweep().bufferCount ); CHECK( sk::kBlockBytes <= sk::kMaxBlockBytes ); } From a56dba79203fbf011c0c4c6857e07bff6e3dc24e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:24:24 -0400 Subject: [PATCH 55/73] test(lexical): the empty length bucket and empty longRows are gated, not guessed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249670 (src/lexical.h:269) — REFUTED, and now gated. THE CLAIM: `longRows.data() + longRows.size()` on an empty `longRows` "can perform pointer arithmetic on a null pointer", so matchRow needs two early-return guards. THE EVIDENCE: 1. `null + 0` is a NULL POINTER VALUE, not undefined behaviour — [expr.add]/4, "if the expression P evaluates to a null pointer value and J evaluates to 0, the result is a null pointer value", C++17 onward. This project is C++23. `first != last` is then false and the loop body never runs, which is the same answer the proposed guard returns (kNoRow) by a longer road. 2. The bucketIdx half cannot even reach a non-zero offset: buildLexHeadIndex assigns bucketOff kMaxLen+2 entries and an empty bucketIdx leaves every one of them 0, so the worst case there is also null + 0. 3. Empirical, on this toolchain, under the project's own G1 set (-fsanitize=address,undefined,integer,float-divide-by-zero,float-cast-overflow -fno-sanitize-recover=all): a standalone probe of `v.data() + v.size()` on an empty vector exits 0 with no report, and so does the new arm below. The path IS reachable — longRows is empty on every ordinary match table, and a 65+ byte corpus token whose lowercased head is in the head set walks straight into it — so the right answer is not "it cannot happen" but "it happens, and it is defined". Adding the guards would add two branches and a second way to spell kNoRow for no behaviour change. GATE ARM instead, test/verify_strkern.cpp (run by test/strkerncheck.sh arm 1, the full G1 sanitizer build, and again by the Rosetta x86_64 arms): "LexHeadIndex empty length bucket and empty longRows" drives the empty-longRows range with a 70-byte token, an empty length bucket with a 4-byte one, AND the non-empty long path, asserting kNoRow / kNoRow / a real row. Non-vacuous in both directions: it fails if matchRow ever starts dereferencing, and it fails if the long path stops matching. strkerncheck.sh: PASS — 20 test cases / 26 assertions under the full G1 sanitizers (was 19/19), NEON non-vacuity, portability, can-go-red 13/26, sweep corpus, Rosetta 3/3b. --- test/strkerncheck.sh | 5 +++-- test/verify_strkern.cpp | 42 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 0f14cd09e..7f44d219f 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -6,8 +6,9 @@ # DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN, one TEST_CASE per kernel, one CHECK/REQUIRE per assertion, built # beside ripwire_test_csr / ripwire_test_pagerank / ripwire_test_radix. Until 2026-09-10 the same arms # lived in a standalone test/strkern_harness.cpp (14 `checkf` arms) and test/emitescape_harness.cpp (4); -# the doctest target carries all 18 plus one — a compiled-path assertion — and this gate prints both -# counts so a lost arm is arithmetic, not a feeling. +# the doctest target carries all 18 plus a compiled-path assertion and, since the #127 review round, a +# LexHeadIndex empty-bucket case; this gate prints both counts so a lost arm is arithmetic, not a +# feeling, and MIN_ASSERTIONS is a FLOOR, never an exact expectation. # # THE TARGET IS BUILT THREE TIMES, and each build is a different question: # diff --git a/test/verify_strkern.cpp b/test/verify_strkern.cpp index c415ef864..9771cb364 100644 --- a/test/verify_strkern.cpp +++ b/test/verify_strkern.cpp @@ -65,6 +65,7 @@ #include "infra/strkern.h" #include "infra/jsonesc.h" #include "lexindex.h" +#include "lexical.h" // LexHeadIndex — the empty-bucket arm below is this header's #include "serialize.h" #include "harnesscommon.h" // DeterministicRng — the sanitizer-clean generator the SIMD harnesses share @@ -1108,6 +1109,47 @@ TEST_CASE( "strkern: the compiled path is the one this target claims" ) CHECK( sk::kBlockBytes <= sk::kMaxBlockBytes ); } +// ── LexHeadIndex: the EMPTY bucket and the EMPTY longRows (CodeRabbit #127 / 3985249670) ───────────── +// matchRow picks its scan range as `bucketIdx.data() + bucketOff[len]` for a token of at most kMaxLen +// bytes and as `longRows.data() … + longRows.size()` above it. On the ordinary table NO row is longer +// than 64 bytes, so `longRows` is EMPTY and `data()` may be null — and a 65+ byte corpus token whose +// lowercased head is in the head set reaches exactly that expression. A length bucket that holds no row +// is the same shape one level down. +// +// `null + 0` is a null pointer value, not undefined behaviour ([expr.add]/4, C++17 onward; this project +// is C++23), and `first != last` is then false, so the loop body never runs. This arm is that claim in +// executable form, under the same -fsanitize=address,undefined,integer,-fno-sanitize-recover=all build +// arm 1 runs: it drives BOTH empty ranges and asserts the answer is kNoRow. It also drives the NON-empty +// long path, so it is not a test of two early returns. +TEST_CASE( "strkern: LexHeadIndex empty length bucket and empty longRows" ) +{ + using rw::LexHeadIndex; + + // A table whose every row is short: longRows is empty, and most length buckets are empty too. + const std::vector< std::string > shortTable{ "alpha", "beta", "gamma" }; + const auto shortTokOf = [ & ]( std::size_t m ) -> const std::string& { return shortTable[ m ]; }; + const LexHeadIndex shortIx = rw::buildLexHeadIndex( shortTable.size(), shortTokOf ); + REQUIRE( shortIx.longRows.empty() ); + CHECK( shortIx.longRows.data() + shortIx.longRows.size() == shortIx.longRows.data() ); + + // a 70-byte token whose head 'a' IS in the head set — the length bucket does not exist, so the + // kMaxLen branch is not taken and the empty longRows range is what decides the answer. + const std::string longTok( 70, 'a' ); + CHECK( shortIx.matchRow( longTok.data(), longTok.size(), shortTokOf ) == LexHeadIndex::kNoRow ); + // an in-range length whose bucket is empty (no 4-byte row starts with 'a'), head still in the set + const std::string fourA = "aaaa"; + CHECK( shortIx.matchRow( fourA.data(), fourA.size(), shortTokOf ) == LexHeadIndex::kNoRow ); + // the rows that DO exist still resolve — the arm is not passing because everything returns kNoRow + CHECK( shortIx.matchRow( shortTable[ 1 ].data(), shortTable[ 1 ].size(), shortTokOf ) == 1u ); + + // NON-EMPTY longRows: one 70-byte row, so the long branch has something to scan and hits. + const std::vector< std::string > longTable{ "alpha", std::string( 70, 'a' ) }; + const auto longTokOf = [ & ]( std::size_t m ) -> const std::string& { return longTable[ m ]; }; + const LexHeadIndex longIx = rw::buildLexHeadIndex( longTable.size(), longTokOf ); + REQUIRE( longIx.longRows.size() == 1u ); + CHECK( longIx.matchRow( longTok.data(), longTok.size(), longTokOf ) == 1u ); +} + TEST_CASE( "strkern: A1 classMasks over all 256 byte values, every offset and length" ) { std::string every; From 8961bd0b91858fcb600f93d7afa2597542249682 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:26:34 -0400 Subject: [PATCH 56/73] test(astqueryregex): arm D asserts the dynamic predicate MATCHED, not merely that a root was emitted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249714 (test/astqueryregexcheck.sh:125) — the PREMISE is REFUTED; the weak assertion it pointed at is real and is fixed. THE CLAIM: "No fixture call satisfies that relation, so arm D always reports `emitted no match root` on correct behavior." THE EVIDENCE: the probe is (#match? @f @a) over (call_expression function: (identifier) @f arguments: (argument_list (identifier) @a)) so the regex is the ARGUMENT's text and the subject is the CALLEE's. The fixture's `strcpy( dst, srcText )` in b.cpp and `strcat( d, s )` in c.c give lhs="strcpy"/rhs="s" and lhs="strcat"/rhs="s" — regex_search("strcpy", regex("s")) is TRUE. The recorded golden has pinned the result since it was written: strcpys Arm D passes today and passed before this branch. No fixture change is needed, and adding `dynamicProbe(dynamicProbe)` would have forced a golden re-record for nothing. WHAT WAS ACTUALLY WEAK: the assertion, not the fixture. `grep -q ' Date: Thu, 10 Sep 2026 22:26:34 -0400 Subject: [PATCH 57/73] test(capsweep): the metavariable arm belongs INSIDE the run-corpus success branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249719 (test/capsweepcheck.sh:297) — VALID. The block was indented as if it were part of the `else` branch opened at line 245, but that branch closed at 284. It therefore ran after a FAILED run-corpus too, where $TMP/rc-screen.tsv does not exist: awk fails, and the gate prints a second, invented failure — "(J) the metavariable row did not answer" — about a run that never produced a single record. Two failures reported, one cause, and the second one points away from it. Moved inside the branch (the `fi` now closes after it), with the reason recorded in the comment so it cannot drift back out. No arm changed its expectation. capsweepcheck.sh: ALL PASS (24 arms), including both (J) arms and the (J) control. --- test/capsweepcheck.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/test/capsweepcheck.sh b/test/capsweepcheck.sh index 94e5e466d..0b187eae8 100755 --- a/test/capsweepcheck.sh +++ b/test/capsweepcheck.sh @@ -281,13 +281,16 @@ else else no "(J) the corpus-tmp destination was not written outside the corpus: $( grep -m1 stub-tmp "$rc_out" )" fi -fi - # (J) A $NAME OUTSIDE THE HARNESS'S NAMESPACE IS NOT AN ENVIRONMENT REFERENCE. The first cut of the # rule above refused `--pattern='rankGraphTeleport($A, $B, $C)'` — a tree-sitter pattern whose $A/$B/$C # are METAVARIABLES — as "unexpanded", turning a legitimate corpus row into a non-answer. shlex.split # has already dropped the quoting by then, so single-quoted and double-quoted cannot be told apart: # naming the namespace is what makes the rule decidable. + # + # INSIDE the g_ok branch (CodeRabbit #127 / 3985249719): it reads $TMP/rc-screen.tsv, which only a + # successful run-corpus writes. Indented as if it belonged here but sitting after the `fi`, it ran on a + # FAILED run too — awk then failed on a missing file and the gate printed a second, invented "(J) the + # metavariable row did not answer" for a run that never produced one record. if grep -q 'unexpanded: \$A' "$rc_out"; then no "(J) a tree-sitter metavariable was refused as an unexpanded environment variable" elif awk -F'\t' '/stub-metavar/ { exit !($4 == "ok") }' "$TMP/rc-screen.tsv"; then @@ -295,6 +298,7 @@ fi else no "(J) the metavariable row did not answer: $( grep -- 'stub-metavar' "$TMP/rc-screen.tsv" )" fi +fi # (J) control — a destination that resolves INSIDE the corpus is refused. `--cache=`, `--export=` and # `--html=` all take one, and run_corpus runs with cwd=corpus, so this is the surface that put a 10.4 MB From 41e08676a32e4afb894d3bd137a68c89813d6268 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:26:34 -0400 Subject: [PATCH 58/73] test(eviction): arm (h) finds the LEAN blob by name, not whatever the filesystem lists first MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249724 (test/evictioncheck.sh:305) — VALID. `find … -name 'ripwire-*.bin' | head -1` returns whichever blob the directory happens to list first, while the sed on the next line only matches `ripwire-<16hex>-lean.bin`. Let the priming run leave a `-rich.bin` and be listed first and ROOTHEX3 keeps the entire basename, the `^[0-9a-f]{16}$` check fails, and arm (h) goes red for a directory-ordering reason on a tool that is behaving correctly. The shell fact, directly: $ basename ripwire-0123456789abcdef-rich.bin \ | sed -E 's/^ripwire-([0-9a-f]{16})-lean\.bin$/\1/' ripwire-0123456789abcdef-rich.bin # not 16 hex — arm (h) red Arm (k) at the bottom of the same file already uses the precise pattern for the same job, so this was one site out of step with its own gate. evictioncheck.sh: ALL PASS — (h) primed: this root's lean blob names root key 9ee2821361558d0d, and every (h)/(i)/(j)/(k)/(l) arm after it. --- test/evictioncheck.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/evictioncheck.sh b/test/evictioncheck.sh index d2ac1119b..eef567031 100755 --- a/test/evictioncheck.sh +++ b/test/evictioncheck.sh @@ -301,7 +301,12 @@ R3="$TMP3/repo"; mkdir -p "$R3" printf 'int pinme( void )\n{\n return 1;\n}\n' > "$R3/f.cpp" env -u XDG_CACHE_HOME TMPDIR="$CB3" "$BIN" "$R3" >/dev/null 2>"$TMP3/prime.err" -OWN3="$( find "$CD3" -mindepth 1 -maxdepth 2 -name 'ripwire-*.bin' 2>/dev/null | head -1 )" +# -name 'ripwire-*-lean.bin', not 'ripwire-*.bin' (CodeRabbit #127 / 3985249724): the sed below only +# matches the LEAN basename, and the priming run can leave a -rich.bin beside it. `head -1` over the wider +# glob then returns whichever the filesystem happens to list first, ROOTHEX3 keeps the whole basename, and +# the 16-hex check goes red for a directory-ordering reason. Arm (k) at the bottom of this file already +# uses the precise pattern for the same job. +OWN3="$( find "$CD3" -mindepth 1 -maxdepth 2 -name 'ripwire-*-lean.bin' 2>/dev/null | head -1 )" ROOTHEX3="$( basename "${OWN3:-none}" | sed -E 's/^ripwire-([0-9a-f]{16})-lean\.bin$/\1/' )" if printf '%s' "$ROOTHEX3" | grep -qE '^[0-9a-f]{16}$'; then ok "(h) primed: this root's lean blob names root key $ROOTHEX3" From 71ca36fcb8855f4ecfc18a6dd2ae6279be2e05ea Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:26:34 -0400 Subject: [PATCH 59/73] test(portablebuild): the aarch64 arm fails on CONFIGURE_FAILED instead of reporting PASS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249736 (test/portablebuildcheck.sh:122) — VALID. Arm #2c's expectation is an ABSENCE — an aarch64 target must not be handed an x86 -march. CONFIGURE_FAILED contains no '-march=x86' either, so a broken aarch64-specific CMake path satisfied the arm and the portability gate reported PASS. #2b, whose expectation is a PRESENCE, already carried the sentinel check; this is the one arm where the absence shape hid it. Same sentinel, same wording, ahead of the flag test. CAN GO RED, observed: with `armFlags="CONFIGURE_FAILED"` forced, FAIL #2c aarch64 probe configure failed outright: portablebuildcheck.sh unmutated: ALL PASS — #2c aarch64 target stays generic ('-O2;-ffast-math;-fno-finite-math-only'). --- test/portablebuildcheck.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/portablebuildcheck.sh b/test/portablebuildcheck.sh index 46e20b8e0..efb0b0060 100755 --- a/test/portablebuildcheck.sh +++ b/test/portablebuildcheck.sh @@ -119,7 +119,12 @@ fi # ── #2c: the floor is x86-ONLY — an aarch64 Linux target must not be handed an x86 -march ───────────── armFlags="$( PROBE_PROC=aarch64 run_probe "$TMP/arm" -DRIPWIRE_PRETEND_LINUX=ON )" -if printf '%s' "$armFlags" | grep -q -- '-march=x86'; then +# The sentinel FIRST (CodeRabbit #127 / 3985249736): CONFIGURE_FAILED contains no '-march=x86' either, so +# without this arm a broken aarch64-specific CMake path reported PASS on this portability gate — the exact +# shape #2b above already guards against, missing on the one arm whose expectation is an ABSENCE. +if [ "$armFlags" = "CONFIGURE_FAILED" ]; then + no "#2c aarch64 probe configure failed outright: $(tail -5 "$TMP/arm/configure.log" 2>/dev/null)" +elif printf '%s' "$armFlags" | grep -q -- '-march=x86'; then no "#2c an aarch64 target was handed an x86 architecture flag: '$armFlags'" else ok "#2c aarch64 target stays generic (NEON is baseline there, no flag needed): '$armFlags'" From 69e57e58d8e10580704df137b759ce90c1515232 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:30:10 -0400 Subject: [PATCH 60/73] fix(cache): cacheBlobRootKey reads a key field terminated by '.', so the MCP blob is pinnable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249706 (src/quality.h:2130) — VALID. `mcpCachePath` builds `ripwire-mcp-.cache` through the same rootKeyedCachePath the lean/rich parse cache uses, but it is the only family whose key field is terminated by '.' instead of '-'. cacheBlobRootKey split on '-' alone, so the last field came out as `.cache` — 22 bytes, not hex16 — and the function returned an EMPTY key for the one family whose entire name IS a root key. The consequence is in evictBySizeBudget. `pinRootKey.empty()` pins nothing, so: * a sweep triggered by a CLI run pinned that root's lean and rich blobs and evicted the MCP index of the SAME root — the server then paid a full re-parse; * a sweep triggered by the MCP server itself computed pinRootKey from its own `ripwire-mcp-…cache` keepPath, got "", and pinned nothing but that one file — every other family of its own root became evictable. This is P1-1's 206 s ping-pong, surviving in the one family P1-1 did not name. A field now ends at the next '-' OR at the next '.' (find_first_of). Every other family's key is dash-terminated and reached the same way before, so no existing name's reading moves; '.' occurs only in the suffix, so no false key can be manufactured. WHY IT SURVIVED THE GATE: test/evictioncheck.sh's shell mirror `blobrootkey` strips `\.(bin|cache)$` BEFORE splitting on '-', i.e. it always read the MCP name correctly. The gate's reading and the binary's had silently diverged, and arm (k) never primed an MCP blob, so nothing asked the question. GATE ARMS, test/evictioncheck.sh: * (h) seeds `ripwire-mcp-.cache` for the pinned root beside its rich sibling and asserts it SURVIVES the over-budget sweep. * (k) seeds the MCP family into the all-families dir, so the one-root-key arm and the shell-mirror agreement arm now cover the '.'-terminated field. CAN GO RED, observed against the pre-fix binary: FAIL (h) the MRU root's ripwire-mcp-.cache was EVICTED — cacheBlobRootKey cannot read a '.'-terminated key field After the fix: evictioncheck.sh ALL PASS (34 arms). BYTE-IDENTICAL on both external corpora (--top-k=100000, --for, --pack-task, --no-cache): go 10,415,057 B and canyonraid48 7,229,007 B all identical. ripwire's own map moves by +2 symbols / +4 edges — the new TEST_CASE and lambda in test/verify_strkern.cpp, which is itself indexed; that is the source changing, not the tool. --- src/quality.h | 17 +++++++++++++---- test/evictioncheck.sh | 23 +++++++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/src/quality.h b/src/quality.h index 04bdf5d09..59fd24d49 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2111,20 +2111,29 @@ inline std::string cacheBlobRootKey( std::string_view blobName ) noexcept return std::string{}; // content- or file-addressed, root-independent by design — see above } + // A field ends at the next '-' OR at the '.' that opens the suffix. The dash-only scan this replaces + // could not read `ripwire-mcp-.cache` (mcpindex.h::mcpCachePath): its last field came out as + // `.cache`, 22 bytes, not hex16, so the function returned an EMPTY key for the one family + // whose name is nothing but a root key. The consequence is in evictBySizeBudget — an empty key pins + // nothing, so the byte-budget sweep would evict the MCP index blob of the very root it was serving + // while pinning that root's lean and rich blobs, and the MCP server paid a full re-parse for it. + // (CodeRabbit #127 / 3985249706. test/evictioncheck.sh's shell mirror `blobrootkey` already stripped + // `\.(bin|cache)$` before splitting, so the gate's reading and the binary's had silently diverged — + // arm (k) never primed an MCP blob, which is why nothing caught it.) std::size_t at = 0; while( at < blobName.size() ) { - const std::size_t dash = blobName.find( '-', at ); - const std::string_view field = blobName.substr( at, dash == std::string_view::npos ? std::string_view::npos : dash - at ); + const std::size_t sep = blobName.find_first_of( "-.", at ); + const std::string_view field = blobName.substr( at, sep == std::string_view::npos ? std::string_view::npos : sep - at ); if( isHex16( field ) ) { return std::string( field ); } - if( dash == std::string_view::npos ) + if( sep == std::string_view::npos ) { break; } - at = dash + 1; + at = sep + 1; } return std::string{}; } diff --git a/test/evictioncheck.sh b/test/evictioncheck.sh index eef567031..7c767458c 100755 --- a/test/evictioncheck.sh +++ b/test/evictioncheck.sh @@ -318,6 +318,14 @@ fi # the OLDEST blob in the dir, which is exactly what the pre-change oldest-first sweep deletes first. SIB3="$CD3/ripwire-$ROOTHEX3-rich.bin" truncate -s 1200M "$SIB3" +# …and the SAME root's MCP index blob. It is the one family whose name is nothing but a root key, and it +# is the only one that ends `.cache` rather than `-.bin` — so quality.h::cacheBlobRootKey, which +# split on '-' alone, read its last field as `.cache` (22 bytes, not hex16) and returned an EMPTY +# key. An empty key pins nothing: the sweep kept this root's lean and rich blobs and evicted the MCP index +# of the very root it was serving, and the MCP server paid a full re-parse for it. Zero-size on purpose — +# survival is the question, not bytes (CodeRabbit #127 / 3985249706). +MCP3="$CD3/ripwire-mcp-$ROOTHEX3.cache" +: > "$MCP3" sleep 1 # a DIFFERENT root's blob, newer and bigger — the one an oldest-first sweep would keep, and the one the # fixed sweep must take instead. @@ -338,6 +346,8 @@ grep -q 'n="pinme"' "$TMP3/run.xml" 2>/dev/null && ok "(h) run output still corr || no "(h) the MRU root's sibling family was EVICTED — the sweep still takes the blob this root is about to need" [ ! -e "$OTHER3" ] && ok "(h) the OTHER root's blob is what the sweep took instead" \ || no "(h) the other root's blob survived — the sweep did not free the bytes it needed" +[ -e "$MCP3" ] && ok "(h) the MRU root's MCP index blob survives too — ripwire-mcp-.cache reads as THIS root" \ + || no "(h) the MRU root's ripwire-mcp-.cache was EVICTED — cacheBlobRootKey cannot read a '.'-terminated key field" [ -s "$TMP3/run.err" ] && ok "(h) the eviction is DISCLOSED on stderr (was 0 bytes before this change)" \ || no "(h) an eviction happened with ZERO disclosure — the honesty rule does not reach the cache layer" @@ -477,6 +487,19 @@ EOF_K primeallfamilies "$CB6" "$R6" +# The MCP index family, seeded by hand: `ripwire wrap` is not something this gate can drive, but the family +# exists (mcpindex.h::mcpCachePath → quality::rootKeyedCachePath( root, "ripwire-mcp-", ".cache" )) and it is +# the one whose key field is terminated by '.' rather than '-'. Without it in the dir the key-agreement arm +# below never asked the question that #127/3985249706 answered. +MCPKEY6="$( find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*-lean.bin' 2>/dev/null | head -1 )" +MCPKEY6="$( basename "${MCPKEY6:-none}" | sed -E 's/^ripwire-([0-9a-f]{16})-lean\.bin$/\1/' )" +if printf '%s' "$MCPKEY6" | grep -qE '^[0-9a-f]{16}$'; then + : > "$CD6/ripwire-mcp-$MCPKEY6.cache" + ok "(k) the MCP index family is present (ripwire-mcp-$MCPKEY6.cache) — the '.'-terminated key field" +else + no "(k) could not derive this root's key from its lean blob, so the MCP family could not be seeded" +fi + blobs6="$( find "$CD6" -mindepth 1 -maxdepth 2 -type f -name 'ripwire-*' 2>/dev/null | wc -l | tr -d ' ' )" [ "$blobs6" -ge 4 ] && ok "(k) primed $blobs6 cache blobs across the families one session writes" \ || no "(k) only $blobs6 blob(s) written — the families under test were never primed" From 556925b1bcde2bfd7a95873a9a8c5f32bb57b95d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:36:10 -0400 Subject: [PATCH 61/73] =?UTF-8?q?fix(errormask):=20a=20comment=20that=20OP?= =?UTF-8?q?ENS=20the=20block=20does=20not=20close=20it=20=E2=80=94=20confi?= =?UTF-8?q?rm=20on=20the=20raw=20bytes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249701 (src/lintrules.h:1108) — VALID. errorMaskBlockIsCommentOnly proved only that a comment OPENS the interior: everything after the first `//`, `/*` or `#` was accepted unread. `catch (e) { /* ignore */ recover() }` is valid JavaScript, carries no ';' and no inner '{', and was therefore reported as a swallow — while the block handles the error. That is a MANUFACTURED finding, the one direction §Q-DIAL-6's own two stated floors forbid ("both directions of the imprecision lose recall rather than manufacturing a finding"). IT CANNOT BE FIXED ON THE FLATTENED TEXT, which is why the fix is shaped the way it is. makeAstMatch — the ONE span cut — scrubs '\n' to ' ', and a `//` comment ends at a newline that is no longer there. `{ // ignore recover() }` and `{ // ignore recover() }` are the same bytes after the scrub; the first handles, the second swallows. The information only exists in the file. So: errorMaskBlockIsCommentOnly stays, renamed in role to a PREFILTER, and findErrorMasking CONFIRMS every row it admits through a comment against the block's raw bytes (errorMaskCommentConsumesBlock): `/* … */` spans to its closer, `//` and `#` run to end of line, only whitespace between, and code anywhere means handler. Three properties hold it in place: * a bare `{}` skips the confirm (errorMaskBlockIsBareBraces) — no comment to mis-read, and the read cost stays at "files that have a comment-shaped candidate", one-entry memo, astQuery already sorting (file, startByte) so a single slot is the whole cache; * the raw slice is `m.text.size()` bytes — the SAME cut, since the scrub is byte-for-byte — so §Q-DIAL-6's 120-byte floor is preserved character for character; * an unreadable or moved file DROPS the row (DEGRADED_PATH_ALERT): a finding that cannot be substantiated is not reported. The confirm can only REMOVE rows, never add one. GATE, test/qddialscheck.sh §6: a JS fixture (JS is where ASI lets a handler body carry no ';' and no inner '{'), all three functions committed as real `return -1` handlers so the working edit makes each row NEW debt. guardedJs is the POSITIVE control (a true comment-only swallow, still reported); recoveredJs is the `/* */`-then-code handler; lineCommentJs is the `//`-then-next-line handler that flattened text cannot decide at all. CAN GO RED, observed — the same gate against the pre-fix binary: FAIL error-masking: code AFTER a /* */ comment was counted as a swallow FAIL error-masking: code on the line after a // comment was counted as a swallow recover() }` and +// `{ // ignore recover() }` are the same 23 bytes after the scrub, and the first is a handler while the +// second is a swallow. So the confirm reads the block's RAW bytes and asks the only question that decides +// it — does comment text consume the WHOLE interior? +// +// /* … */ spans to its closer; an unterminated one is NOT comment-only (it cannot be, the block closed) +// // # run to the end of THEIR line — the fact the scrub destroyed +// between only spaces, tabs, CR and LF +// +// `raw` is the block's bytes cut to exactly the length astQuery cut its text to, so the 120-byte floor +// §Q-DIAL-6 states is preserved character for character: this confirm can only REMOVE rows, never add one. +inline bool errorMaskCommentConsumesBlock( std::string_view raw ) noexcept +{ + const auto isSpace = []( char c ) noexcept { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; }; + + std::string_view t = raw; + while( !t.empty() && isSpace( t.front() ) ) { t.remove_prefix( 1 ); } + while( !t.empty() && isSpace( t.back() ) ) { t.remove_suffix( 1 ); } + if( t.size() < 2 || t.front() != '{' || t.back() != '}' ) + { + return false; + } + + const std::string_view mid = t.substr( 1, t.size() - 2 ); + std::size_t at = 0; + while( at < mid.size() ) + { + if( isSpace( mid[ at ] ) ) + { + ++at; + continue; + } + if( mid.compare( at, 2, "/*" ) == 0 ) + { + const std::size_t close = mid.find( "*/", at + 2 ); + if( close == std::string_view::npos ) + { + return false; // the block closed but the comment did not — not decidable as a swallow + } + at = close + 2; + continue; + } + if( mid.compare( at, 2, "//" ) == 0 || mid[ at ] == '#' ) + { + const std::size_t nl = mid.find( '\n', at ); + if( nl == std::string_view::npos ) + { + return true; // the line comment runs to the end of the interior + } + at = nl + 1; + continue; + } + return false; // code survives inside the block — a handler, not a swallow + } + return true; +} + +// A brace pair with nothing at all between them. Split out from errorMaskBlockIsEmpty so the caller can +// tell WHICH half answered: this one needs no confirm (there is no comment to mis-read), the comment half +// does. +inline bool errorMaskBlockIsBareBraces( std::string_view collapsed ) noexcept { std::string stripped; for( char c : collapsed ) @@ -1118,7 +1186,15 @@ inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept stripped.push_back( c ); } } - return stripped == "{}" || errorMaskBlockIsCommentOnly( collapsed ); + return stripped == "{}"; +} + +// THE PREFILTER, over astQuery's flattened span text. Cheap and deliberately over-accepting on its comment +// half — findErrorMasking confirms every row this admits through a comment against the block's RAW bytes +// (errorMaskCommentConsumesBlock). Never call this alone to decide a finding. +inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept +{ + return errorMaskBlockIsBareBraces( collapsed ) || errorMaskBlockIsCommentOnly( collapsed ); } // One error-masking hit: the suppressing block's file + start byte (so a caller can attribute it to the @@ -1157,6 +1233,11 @@ inline std::vector findErrorMasking( const IngestResult& ing ) // AstMatch per CAPTURE, so a swallow rule yields both a @p hit and a @m hit. We keep only the @m block by // its emptiness signature: @p (a bare identifier "catch"/"then") is never "{}", and for non-emptyOnly // Python rules @p does not exist, so every emitted capture is the block. Route by tag → rule. + // one-entry raw-bytes memo for the confirm below: astQuery already sorts (file, startByte, tag), so the + // candidates of one file arrive together and a single slot is the whole cache. + std::uint32_t rawFileId = ~std::uint32_t( 0 ); + std::string rawBytes; + for( const AstMatch& m : astQuery( ing, specs ) ) { std::size_t r = 0; @@ -1180,6 +1261,35 @@ inline std::vector findErrorMasking( const IngestResult& ing ) { continue; // the @p identifier capture is dropped here too (never "{}") } + // THE CONFIRM (CodeRabbit #127 / 3985249701). The prefilter above cannot see where a `//` comment + // ends, because astQuery scrubbed the newline that ended it — so a block admitted through its + // COMMENT half is re-asked of the file's own bytes. A bare `{}` needs no confirm: there is no + // comment there to mis-read, and skipping it keeps the cost at "one read per file that has a + // comment-shaped candidate", which on this repo's history is a handful of files, not the corpus. + // + // `m.text.size()` IS the cut length makeAstMatch used (the scrub is byte-for-byte), so the raw + // slice is the same span — the 120-byte floor §Q-DIAL-6 discloses is preserved exactly. An + // UNREADABLE file degrades to dropping the row: a finding we cannot substantiate is not reported. + if( rule.emptyOnly && !errorMaskBlockIsBareBraces( m.text ) ) + { + if( m.fileId != rawFileId ) + { + rawFileId = m.fileId; + rawBytes.clear(); + if( !docparse::detail::readWholeFile( diskPath( ing, m.fileId ), rawBytes ) ) + { + DEGRADED_PATH_ALERT( "lintrules: error-mask confirm cannot re-read the block's file" ); + } + } + if( std::size_t( m.startByte ) + m.text.size() > rawBytes.size() ) + { + continue; // the file moved under us, or could not be read — do not assert a swallow + } + if( !errorMaskCommentConsumesBlock( std::string_view( rawBytes ).substr( m.startByte, m.text.size() ) ) ) + { + continue; // a comment OPENS the block but code follows it — that is a handler + } + } out.push_back( { m.fileId, m.startByte, m.line, std::string( rule.id ) } ); } diff --git a/test/qddialscheck.sh b/test/qddialscheck.sh index 0fbf5b2fc..8ae5666c2 100755 --- a/test/qddialscheck.sh +++ b/test/qddialscheck.sh @@ -347,6 +347,26 @@ int logged( int n ){ catch( const std::runtime_error& e ) { std::fprintf( stderr, "bad" ); return -2; } } CPP +# The JS half (CodeRabbit #127 / 3985249701). JavaScript is where the review's counterexample lives, +# because ASI means a handler body can carry NO ';' and NO inner '{' — which is everything the flattened +# prefilter can see. All three start life as a real `return -1` handler so the working edit makes each row +# NEW debt, which is the only kind --quality-delta reports. +cat > "$EM/src/m.js" <<'JS' +function riskyJs( n ) { return n } +function recoverJs( n ) { return n + 1 } +function guardedJs( n ) { + try { return riskyJs( n ) } + catch ( e ) { return -1 } +} +function recoveredJs( n ) { + try { return riskyJs( n ) } + catch ( e ) { return -1 } +} +function lineCommentJs( n ) { + try { return riskyJs( n ) } + catch ( e ) { return -1 } +} +JS ( cd "$EM" && git add -A >/dev/null 2>&1 && git commit -qm base >/dev/null 2>&1 ) python3 - "$EM/src/m.cpp" <<'PY' import sys @@ -354,6 +374,19 @@ p=sys.argv[1]; s=open(p).read() s=s.replace("catch( const std::runtime_error& e ) { return -1; }", "catch( const std::runtime_error& e ) { /* deliberately ignored */ }") open(p,"w").write(s) +j=p.replace("m.cpp","m.js"); t=open(j).read() +def sub(fn, body): + global t + old = "function %s( n ) {\n try { return riskyJs( n ) }\n catch ( e ) { return -1 }\n}" % fn + assert old in t, fn + t = t.replace(old, "function %s( n ) {\n try { return riskyJs( n ) }\n catch ( e ) %s\n}" % (fn, body)) +# a REAL swallow — the comment is the whole interior. Must be reported. +sub("guardedJs", "{ /* deliberately ignored */ }") +# a comment OPENS the block, then code runs. A handler. Must NOT be reported. +sub("recoveredJs", "{ /* fall back */ recoverJs( n ) }") +# the one flattened text cannot decide: the newline that ends the // comment is scrubbed to a space. +sub("lineCommentJs", "{ // fall back\n recoverJs( n )\n }") +open(j,"w").write(t) PY OEM="$( cd "$EM" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" row "$OEM" error-masking guarded >/dev/null \ @@ -362,6 +395,23 @@ row "$OEM" error-masking guarded >/dev/null \ row "$OEM" error-masking logged >/dev/null \ && { no "error-masking: a catch that LOGS and returns was counted — a statement survives in it"; rows "$OEM"; } \ || ok "error-masking: a catch carrying a real statement is not a swallow" + +# NON-VACUITY FIRST: the JS swallow must be reported, or the two negative arms below prove nothing. +row "$OEM" error-masking guardedJs >/dev/null \ + && ok "error-masking: a comment-only JS catch block is a swallow (the positive control)" \ + || { no "error-masking: the comment-only JS catch was missed — the two arms below are vacuous"; rows "$OEM"; } +# A COMMENT THAT OPENS THE BLOCK DOES NOT CLOSE IT (CodeRabbit #127 / 3985249701). Both bodies open with a +# comment and then run real code; neither holds a ';' or an inner '{', which is everything the flattened +# prefilter can see, so both were reported as swallows even though each HANDLES the error. `lineCommentJs` +# is the one flattened text cannot decide AT ALL: astQuery scrubs the newline that ends a `//` comment, so +# `{ // fall back recoverJs( n ) }` is byte-identical to a block whose entire interior is a comment. The +# confirm re-reads the file's own bytes, which is the only place that distinction still exists. +row "$OEM" error-masking recoveredJs >/dev/null \ + && { no "error-masking: code AFTER a /* */ comment was counted as a swallow — the block handles the error"; rows "$OEM"; } \ + || ok "error-masking: a /* */ comment followed by code is a HANDLER, not a swallow" +row "$OEM" error-masking lineCommentJs >/dev/null \ + && { no "error-masking: code on the line after a // comment was counted as a swallow"; rows "$OEM"; } \ + || ok "error-masking: a // comment followed by code on the next line is a HANDLER, not a swallow" [ "$OEM" = "$( cd "$EM" && "$BIN" . --quality-delta --no-cache 2>/dev/null )" ] \ && ok "error-masking: byte-identical run to run (deterministic)" || no "error-masking: non-deterministic delta" From c7a91322f254519f80fed645f457ce29e53fdcb5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:40:40 -0400 Subject: [PATCH 62/73] fix(mcp): situational_awareness discloses BOTH of its paged arrays MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit #127 finding 3985249704 (src/mcpverbs.h:1269, +1313) — VALID. `blast_radius` and `forgotten` are both windowed by pageWindow with the same limit/offset, and the payload carried no shown count, no total, no has_more and no next_offset for either. A caller could not tell that rows had been omitted and could not construct a next request. The CLI twin has disclosed its blast-radius cut in prose since C1 F-10 (situ.h's situShowingNote: "showing 8 of N files — shown=8 total=N capped=1"), so this was the two dialects saying different things about the same run. THE SHAPE IS --test-gate's, not a new one (§B7.1, situ.h::writeTestGateReportJson): two INDEPENDENT listings in one report is exactly the case pageview.h rule 6 answers with rule 1's noun-prefixed exception, because a bare `shown` next to two arrays is ambiguous in a way `shown_blast_radius` cannot be. So the payload now carries shown_blast_radius / blast_radius_capped — the pair for listing one shown_forgotten / forgotten_capped / forgotten_total — listing two, with its own row population (no other key carried it) total / has_more / next_offset / offset / limit — pageview.h's ONE disclosure under the JSON syntax row, describing the PRIMARY listing (blast_radius, the array it precedes). Its `total` IS blast_radius's total; a second spelling would be the duplicate key jsoncheck #10 pins. Both _capped bits and both shown counts are DERIVED from the rows the document actually emits. pagingDisclosure emits nothing when no window applied, so a bare call (this verb's default is unbounded) gains only the four derived counts. Live, on this repo: bare shown_blast_radius=5 blast_radius_capped=false shown_forgotten=80 forgotten_capped=false forgotten_total=80 limit=2 shown_blast_radius=2 blast_radius_capped=true shown_forgotten=2 forgotten_capped=true forgotten_total=80 total=5 has_more=true next_offset=2 offset=0 limit=2 GATE ARM, test/mcpcontractcheck.sh §G, beside the three live paging probes already there: eleven checks over a bare run and a limit=2 run, every one of them DERIVED (shown must equal the rows served; capped must equal shown < total), so a hand-written constant cannot satisfy it. CAN GO RED, observed against the pre-fix binary: all eleven fail with shown_blast_radius=None … total=None has_more=None next_offset=None offset=None limit=None Green: mcpcontractcheck ALL PASS, mcpattrparitycheck ALL PASS, mcpclidiffcheck ALL PASS, jsoncheck ALL PASS (incl. the --json determinism gate). --- src/mcpverbs.h | 29 ++++++++++++++++++++++++++- test/mcpcontractcheck.sh | 42 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index c621f9159..fe16b3056 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1267,7 +1267,34 @@ inline std::string situationDiffJson( const std::string& root, const std::string // report has printed "(N dependent symbols)" on every such line all along. const PageWindow situJBlast = pageWindow( facts.blastRadius.size(), page.limit, page.offset ); const PageWindow situJForgot = pageWindow( facts.forgotten.size(), page.limit, page.offset ); - out += "],\"blast_radius\":["; + + // ── PAGING DISCLOSURE for the TWO windowed arrays (CodeRabbit #127 / 3985249704) ───────────────── + // Both windows above cut rows, and the payload said so about neither: a caller could not tell that + // rows were omitted, nor construct a next request. This is --test-gate's exact shape (§B7.1, situ.h's + // writeTestGateReportJson) because it is the same situation — TWO INDEPENDENT listings in one report, + // which pageview.h rule 6 answers with the rule-1 noun-prefixed exception rather than a bare `shown` + // that would be ambiguous next to two arrays. So: + // * shown_blast_radius / blast_radius_capped and shown_forgotten / forgotten_capped — the pair per + // LISTING, DERIVED from the rows this document actually emits, not asserted; + // * forgotten_total, because the second listing has no other key carrying its row population; + // * the paging half (total / has_more / next_offset / offset / limit) from pageview.h's ONE + // disclosure under the JSON syntax row, describing the PRIMARY listing — blast_radius, the array + // it precedes. blast_radius's own total is that `total`; a second spelling of it would be the + // duplicate key jsoncheck #10 pins. + // pagingDisclosure emits NOTHING when no window applied, so a bare call (this verb's default is + // unbounded) is byte-unchanged apart from the four derived counts, which are always present. + const std::size_t situJBlastShown = situJBlast.end - situJBlast.begin; + const std::size_t situJForgotShown = situJForgot.end - situJForgot.begin; + char situJPageJson[ kPageDisclosureCap ]; + pagingDisclosure( situJPageJson, sizeof( situJPageJson ), facts.blastRadius.size(), situJBlast.end, + page.limit, page.offset, kJsonPageSyntax ); + out += "],\"shown_blast_radius\":" + std::to_string( situJBlastShown ) + + ",\"blast_radius_capped\":" + ( situJBlastShown < facts.blastRadius.size() ? "true" : "false" ) + + ",\"shown_forgotten\":" + std::to_string( situJForgotShown ) + + ",\"forgotten_capped\":" + ( situJForgotShown < facts.forgotten.size() ? "true" : "false" ) + + ",\"forgotten_total\":" + std::to_string( facts.forgotten.size() ) + + situJPageJson + + ",\"blast_radius\":["; { bool first = true; for( std::size_t i = situJBlast.begin; i < situJBlast.end; ++i ) diff --git a/test/mcpcontractcheck.sh b/test/mcpcontractcheck.sh index e40911e86..e5b4c977b 100755 --- a/test/mcpcontractcheck.sh +++ b/test/mcpcontractcheck.sh @@ -401,6 +401,48 @@ for verb, args, arrayKey in ( ( "find_referencing_symbols", { "path": ROOT, "sym check( len( rows1 ) <= 2 and rows1 != rows2 and p1.get( "next_offset" ) == 2, "(G) %s: limit=2 serves %d rows, offset=2 serves different rows, next_offset=%s" % ( verb, len( rows1 ), p1.get( "next_offset" ) ) ) + +# (G/#127-3985249704) A CUT ARRAY MUST SAY SO. situational_awareness windows TWO independent arrays — +# blast_radius and forgotten — with the same limit/offset, and emitted neither a row count nor a total for +# either: a caller could not tell that rows were dropped, and could not build a next request. The CLI twin +# has disclosed its blast-radius cut in prose all along (situ.h's situShowingNote, "showing N of M files — +# shown=N total=M capped=1"), so this was the two dialects disagreeing about the same run. +# +# The shape is --test-gate's (pageview.h rule 6 + rule 1's noun-prefixed exception): a shown_/_capped pair +# per LISTING, plus the paging half for the PRIMARY one. Both halves are asserted DERIVED — shown must +# equal the rows actually served, capped must equal shown < total — so a hand-written constant cannot +# satisfy this arm. +try: + bare = json.loads( srvG.tool( "situational_awareness", { "path": ROOT } )[ "result" ][ "content" ][ 0 ][ "text" ] ) + cut = json.loads( srvG.tool( "situational_awareness", + { "path": ROOT, "limit": 2, "offset": 0 } )[ "result" ][ "content" ][ 0 ][ "text" ] ) +except Exception as e: + check( False, "(G) situational_awareness paging probe failed: %s" % e ) + bare = cut = None +if bare is not None: + for doc, label in ( ( bare, "bare" ), ( cut, "limit=2" ) ): + for arr, shownKey, cappedKey in ( ( "blast_radius", "shown_blast_radius", "blast_radius_capped" ), + ( "forgotten", "shown_forgotten", "forgotten_capped" ) ): + check( doc.get( shownKey ) == len( doc.get( arr, [] ) ), + "(G) situational_awareness %s: %s=%s matches the %d %s rows served" + % ( label, shownKey, doc.get( shownKey ), len( doc.get( arr, [] ) ), arr ) ) + check( isinstance( doc.get( cappedKey ), bool ), + "(G) situational_awareness %s: %s is present and boolean (rule 1 pairs it with shown)" + % ( label, cappedKey ) ) + # the second listing's own row population, so `forgotten_capped` is checkable by the caller + check( bare.get( "forgotten_total" ) == len( bare.get( "forgotten", [] ) ), + "(G) situational_awareness bare: forgotten_total=%s is the whole forgotten population" + % bare.get( "forgotten_total" ) ) + # …and the CUT run carries the continuation for the primary listing, with total unchanged by the window + check( cut.get( "has_more" ) is True and cut.get( "next_offset" ) == 2 + and cut.get( "limit" ) == 2 and cut.get( "offset" ) == 0 + and cut.get( "total" ) == len( bare.get( "blast_radius", [] ) ), + "(G) situational_awareness limit=2: total=%s has_more=%s next_offset=%s offset=%s limit=%s" + % ( cut.get( "total" ), cut.get( "has_more" ), cut.get( "next_offset" ), + cut.get( "offset" ), cut.get( "limit" ) ) ) + check( cut.get( "blast_radius_capped" ) is ( len( cut.get( "blast_radius", [] ) ) < cut.get( "total", 0 ) ) + and cut.get( "forgotten_capped" ) is ( len( cut.get( "forgotten", [] ) ) < cut.get( "forgotten_total", 0 ) ), + "(G) situational_awareness limit=2: both _capped bits are DERIVED from shown < total, not asserted" ) srvG.close() # ═══ (F) the edit verbs' file identity behind a refusal — these verbs delete code when they are wrong ══════ From 3947c2adcc90a1d7f02b545c00176e8c3b6cd12e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:44:48 -0400 Subject: [PATCH 63/73] docs(tuning): regenerate for situational_awareness's two new disclosure keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generated file, regenerated by its own generator — `python3 bench/capsweep/capsweep.py emit`, never by hand. The mcpverbs.h row's "discloses:" list gains blast_radius_capped and forgotten_capped from the #127/3985249704 fix; nothing else in the document moves, and no measurement changed (the row's probe value and its "1 verb(s) respond" are untouched). test/capsweepcheck.sh arm (C) — "docs/TUNING.md matches bench/capsweep/*.tsv + src/ (37 caps with measured effect)" — green again; it was the arm that caught this. --- docs/TUNING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/TUNING.md b/docs/TUNING.md index 40a80a2a7..e02d15fb8 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -301,7 +301,7 @@ moving. Re-run `prepare|screen|sweep` to refresh them. ### `kBatchCap` = `16` -`src/mcpverbs.h` — discloses: `coboost_commits_capped`, `hits_capped`, `unindexed_candidates_capped` — probe value `128` — **1 verb(s) respond** +`src/mcpverbs.h` — discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `unindexed_candidates_capped` — probe value `128` — **1 verb(s) respond** | invocation | default | at probe | delta | | --- | --- | --- | --- | From 3d88a9e0c5b4f0e559af746931a88a2c6b828455 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:44:48 -0400 Subject: [PATCH 64/73] fix(capsweep): the fingerprint sees CONTENTS, and the split is over execution states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CodeRabbit #127 findings in bench/capsweep/capsweep.py, both VALID. ── 3985249656 (capsweep.py:474) — FINGERPRINT FILE CONTENTS, NOT ONLY NAMES ────────────── fingerprint_corpus recorded a sorted list of paths, so a row that OVERWROTE an existing fixture file left the list identical and every later immutability check passed against a changed corpus. This is not hypothetical: `--cache=$RIPWIRE_CAPSWEEP_TMP` with the variable unexpanded wrote a 10.4 MB blob into the frozen tree, and a second run of that row would have rewritten the same path silently. An arbitrary `run-corpus --binary` can write anything anywhere by construction. Each entry now records `\t\t`, sorted by path. A SYMLINK is digested by its TARGET TEXT, UNFOLLOWED — following it would digest something outside the corpus (and could hang on a cycle), and a retargeted symlink is exactly the change this exists to see; `is_symlink()` is asked before `is_file()`, which follows. assert_corpus_unchanged compares path->(kind,digest) maps, so an edit reports as one `~ path (f:aaaa -> f:bbbb)` row rather than a `+`/`-` pair. A name-only fingerprint file is REFUSED with the re-run instruction rather than read as if it could answer. COST, measured on a `git archive HEAD` freeze of this repo (2,357 entries / 259.2 MiB, warm page cache, 5 runs, load avg 3.3): names only median 0.038 s min 0.037 s type+digest median 0.166 s min 0.165 s +0.128 s per check. The screen phase takes two (one per arm) against arms that each run hundreds of real invocations; it does not register. ── 3985249659 (capsweep.py:676) — THE SPLIT USES EXECUTION STATES ──────────────────────── `sorted(c for c in corpus if base.get(c) != allb.get(c))` compared raw values with no regard for bstate/gstate. `None` means "no answer of some kind" and `0` means "exit 0, printed nothing", which answered() has always defined as no answer. Two shapes were miscounted: * 100 bytes at the default, TIMEOUT (or refusal) when bumped → "different" → counted in the numerator as cap-sensitive. It is a regression the bump introduced. * refused at the default, exit 0 with ZERO bytes when bumped → "different" → listed as a row that "answers only when a cap is bumped", about a row that still answers nothing. Now: `hot` compares BYTES only where BOTH arms answered; `late` is answered(bumped-arm) minus answered(baseline); `lost` (answered, then stopped) and `moved` (two non-answering states that differ) are REPORTED with both states — printed as LOST / STATE rows — and never counted. The written recipe states the numerator rule, so the ratio's population is on the record with it. ── GATES, test/capsweepcheck.sh ────────────────────────────────────────────────────────── The stub corpus gains --stub-lose (answers at the default, refuses when bumped), --stub-zero (refuses at the default, exit 0 / 0 bytes when bumped) and --stub-edit=PATH (rewrites an existing corpus file). Arm (I) gains five checks; arm (K2) is new, with its own control. Denominators updated: 4/7 -> 5/9 answered. CAN GO RED, observed — the SAME gate driven against the pre-change harness: FAIL (I) ... cap-sensitive: 2 of 5 answering rows (40%) [the fix reports 1 of 5, 20%] FAIL (I) --stub-lose was marked cap-sensitive — a bump regression counted as byte sensitivity FAIL (I) --stub-zero was marked cap-sensitive — zero bytes counted as an answer FAIL (I) --stub-zero was listed as BY-CAP — a zero-byte exit 0 is not an answer FAIL (I) the screen records do not say that the numerator needs BOTH arms to have answered FAIL (K2) a row that OVERWROTE a corpus file was measured anyway — the fingerprint is name-only Both controls stayed green in that run, so neither new arm refuses everything. capsweepcheck.sh with the fix: ALL PASS (31 arms). --- bench/capsweep/capsweep.py | 100 +++++++++++++++++++++++++++++++------ test/capsweepcheck.sh | 87 ++++++++++++++++++++++++++++++-- 2 files changed, 167 insertions(+), 20 deletions(-) diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index 02bcce53f..fb2aac0f9 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, os, pathlib, re, shlex, shutil, subprocess, sys, collections +import argparse, hashlib, os, pathlib, re, shlex, shutil, subprocess, sys, collections HERE = pathlib.Path(__file__).resolve().parent REPO = HERE.parent.parent @@ -456,12 +456,32 @@ def assert_no_git_above(corpus): # seen it. A file LIST taken after the freeze and re-checked after every arm sees any of it. FINGERPRINT = 'corpus.filelist' # lives in --scratch, never in the corpus +def file_digest(path): + """sha256 of one file's bytes, read in chunks so a large fixture costs no memory.""" + h = hashlib.sha256() + with open(path, 'rb') as fh: + for chunk in iter(lambda: fh.read(1 << 20), b''): + h.update(chunk) + return h.hexdigest() + def fingerprint_corpus(corpus): - """The corpus's file list, `.git/` excluded. + """The corpus's file list WITH each entry's type and content digest, `.git/` excluded. `.git/` is excluded deliberately and it is the one place a git verb may legitimately write: reading a repository refreshes the index stat cache and can write ORIG_HEAD or a reflog. Those are git's bookkeeping about the fixture, not the tree being measured. Everything else is the subject. + + CONTENTS, not just names (CodeRabbit #127 / 3985249656). A name list cannot see a file being + OVERWRITTEN in place, and overwriting is not a hypothetical: `--cache=$RIPWIRE_CAPSWEEP_TMP` with + the variable unexpanded wrote a 10.4 MB blob into the frozen corpus, and a second run of the same + row would have rewritten the same path — same list, changed subject, every later immutability check + green. An arbitrary `run-corpus --binary` is by construction able to write anything anywhere. + + A SYMLINK is fingerprinted by its TARGET TEXT, unfollowed: following it would digest something + outside the corpus (and could hang on a cycle), and a RETARGETED symlink is exactly the change this + is here to catch. `is_symlink()` is asked FIRST because `is_file()` follows. + + Each line is `\t\t`, sorted by PATH so the file diffs like a list. """ corpus = pathlib.Path(corpus) out = [] @@ -469,9 +489,27 @@ def fingerprint_corpus(corpus): rp = p.relative_to(corpus) if rp.parts and rp.parts[0] == '.git': continue - if p.is_file() or p.is_symlink(): - out.append(str(rp)) - return sorted(out) + if p.is_symlink(): + kind = 'l' + digest = hashlib.sha256(os.readlink(p).encode('utf-8', 'surrogateescape')).hexdigest() + elif p.is_file(): + kind, digest = 'f', file_digest(p) + else: + continue # a directory is not a subject; its files are + out.append('%s\t%s\t%s' % (kind, digest, rp)) + return sorted(out, key=lambda line: line.split('\t', 2)[2]) + +def fingerprint_index(lines): + """{relpath: (kind, digest)} — so a CHANGED file reads as one `~` row, not a `+` and a `-`.""" + out = {} + for line in lines: + parts = line.split('\t', 2) + if len(parts) != 3: + sys.exit('capsweep: %s holds a name-only fingerprint, which cannot see a file being\n' + ' overwritten in place. Re-run `prepare` to record type+digest per entry.' + % FINGERPRINT) + out[parts[2]] = (parts[0], parts[1]) + return out def write_fingerprint(scratch, corpus): pathlib.Path(scratch, FINGERPRINT).write_text('\n'.join(fingerprint_corpus(corpus)) + '\n') @@ -485,16 +523,21 @@ def read_fingerprint(scratch): return [l for l in f.read_text().splitlines() if l] def assert_corpus_unchanged(corpus, before, where): - now = fingerprint_corpus(corpus) - new = sorted(set(now) - set(before)) - gone = sorted(set(before) - set(now)) - if new or gone: + was = fingerprint_index(before) + now = fingerprint_index(fingerprint_corpus(corpus)) + new = sorted(set(now) - set(was)) + gone = sorted(set(was) - set(now)) + edited = sorted(f for f in set(was) & set(now) if was[f] != now[f]) + if new or gone or edited: lines = ['capsweep: the frozen corpus CHANGED during %s — every byte count in this run is a' % where, ' measurement of the harness as much as of the subject.'] lines += [' + %s' % f for f in new[:20]] lines += [' - %s' % f for f in gone[:20]] - if len(new) + len(gone) > 40: - lines.append(' (%d more)' % (len(new) + len(gone) - 40)) + lines += [' ~ %s (%s -> %s)' % (f, was[f][0] + ':' + was[f][1][:12], now[f][0] + ':' + now[f][1][:12]) + for f in edited[:20]] + shown = min(len(new), 20) + min(len(gone), 20) + min(len(edited), 20) + if len(new) + len(gone) + len(edited) > shown: + lines.append(' (%d more)' % (len(new) + len(gone) + len(edited) - shown)) sys.exit('\n'.join(lines)) # ── phases ────────────────────────────────────────────────────────────────────────────────────────── @@ -673,14 +716,33 @@ def screen_core(binary, croot, corpus, bump, out_path, measured_at, before): ' split. A ratio over a population that measured nothing is not a result.' % len(corpus)) - sens = sorted(c for c in corpus if base.get(c) != allb.get(c)) - answ = set(ok) - hot = [c for c in sens if c in answ] - late = [c for c in sens if c not in answ] # answered ONLY under the bumped arm — real signal + # THE SPLIT IS OVER EXECUTION STATES, NOT RAW VALUES (CodeRabbit #127 / 3985249659). `base.get(c)` + # is None for every non-answer — a timeout, a refusal, an unparseable row — and 0 for an exit-0 run + # that printed nothing, which `answered()` already defines as NO answer. Comparing those values + # directly counted two things that are not byte sensitivity: + # * 100 bytes at the default, TIMEOUT when bumped: base=100, allb=None, "different" → counted as + # cap-sensitive. It is a regression the bump introduced, and it inflated the numerator. + # * refused at the default, exit 0 with ZERO bytes when bumped: base=None, allb=0, "different" → + # counted as "answers only when a cap is bumped", about a row that still answers nothing. + # So: compare BYTES only where BOTH arms answered, classify a bumped-only answer with answered() + # over the bumped arm, and report every other transition as what it is. + answBase = set(ok) + answBump = set(c for c in corpus if answered(allb, gstate, c)) + hot = sorted(c for c in corpus if c in answBase and c in answBump and base.get(c) != allb.get(c)) + late = sorted(answBump - answBase) # answered ONLY under the bumped arm — real signal + lost = sorted(answBase - answBump) # answered at the DEFAULT and stopped: a bump regression + moved = sorted(c for c in corpus if c not in answBase and c not in answBump + and (bstate.get(c) != gstate.get(c) or base.get(c) != allb.get(c))) + sens = sorted(set(hot) | set(late)) # the rows cmd_sweep will probe cap by cap recipe = ('split recipe: DENOMINATOR = rows that answered under the BASELINE arm (state=ok, >0 bytes).', 'A row that emits nothing cannot respond to a cap; %d row(s) of %d never answer and are' % (len(corpus) - len(ok), len(corpus)), 'recorded here but excluded from the ratio.', + 'NUMERATOR = rows where BOTH arms answered and the byte counts differ. A row whose STATE', + 'moved between the arms is not byte sensitivity and is reported separately, never counted:', + '%d answered only when bumped, %d stopped answering when bumped, %d moved between two' + % (len(late), len(lost), len(moved)), + 'non-answering states.', 'cap-sensitive=%d of %d answering (%.0f%%); %d row(s) answer only when a cap is bumped.' % (len(hot), len(ok), 100.0 * len(hot) / len(ok), len(late))) write_screen(out_path, corpus, base, allb, sens, measured_at, bstate, gstate, recipe) @@ -688,7 +750,13 @@ def screen_core(binary, croot, corpus, bump, out_path, measured_at, before): 'answering rows respond to NO cap' % (len(corpus), len(ok), len(hot), len(ok), 100.0 * len(hot) / len(ok), len(ok) - len(hot))) for c in late: - print(' %8s %s' % ('BY-CAP', c[:88])) # refused at the default, answers when bumped + print(' %8s %s' % ('BY-CAP', c[:88])) # refused at the default, ANSWERS when bumped + # The two transitions that are NOT cap sensitivity, printed with their states so the reason is on the + # screen rather than inferred from a byte count that was never comparable. + for c in lost: + print(' %8s %s [%s -> %s]' % ('LOST', c[:66], bstate.get(c, '?'), gstate.get(c, '?'))) + for c in moved: + print(' %8s %s [%s -> %s]' % ('STATE', c[:66], bstate.get(c, '?'), gstate.get(c, '?'))) for c in hot[:15]: if base.get(c) is None or allb.get(c) is None: print(' %8s %s' % ('TIMEOUT', c[:88])) diff --git a/test/capsweepcheck.sh b/test/capsweepcheck.sh index 0b187eae8..024dba964 100755 --- a/test/capsweepcheck.sh +++ b/test/capsweepcheck.sh @@ -50,6 +50,8 @@ # passed through as a literal, and the destination resolves outside the corpus. # (K) A CORPUS FINGERPRINT taken before the arms and re-checked after each one: a file created inside # the corpus mid-run aborts and names the path. (B) guards one directory name; this guards the class. +# (K2) …and the fingerprint carries each entry's TYPE and CONTENT DIGEST, so a file OVERWRITTEN in +# place aborts too. A name list sees a creation and is blind to a rewrite of the same path. # (L) A GIT REPOSITORY ABOVE the corpus is refused — ripwire walks up for .git in its own code. # (M) THE HISTORY FIXTURE: `git archive HEAD` leaves no .git, so the git verbs measured their degraded # path. Three commits over the same files plus a dirty tree; a tree missing them is REFUSED. @@ -214,6 +216,16 @@ for a in "$@"; do --stub-refuse) exit 3 ;; --stub-tmp=*) d="${a#--stub-tmp=}"; mkdir -p "$d" 2>/dev/null; : > "$d/wrote-here"; printf 'tmp=%s' "$d"; exit 0 ;; --stub-litter) : > "capsweep-litter.txt"; printf '%050d' 0; exit 0 ;; + # The two STATE TRANSITIONS the split must not read as byte sensitivity (#127 / 3985249659). + # --stub-lose: answers 100 B at the DEFAULT and REFUSES when the cap is bumped. base=100, allb=None, + # "different" — the raw comparison counted it as cap-sensitive, inflating the numerator with a + # regression the bump introduced. + # --stub-zero: refuses at the DEFAULT and exits 0 with ZERO bytes when bumped. base=None, allb=0, + # "different" — counted as "answers only when a cap is bumped", about a row that still answers + # nothing; answered() has defined zero bytes as no answer all along. + --stub-lose) if [ -n "${RWCAP_kStubRowCap:-}" ]; then exit 3; else printf '%0100d' 0; exit 0; fi ;; + --stub-zero) if [ -n "${RWCAP_kStubRowCap:-}" ]; then exit 0; else exit 3; fi ;; + --stub-edit=*) printf 'x' > "${a#--stub-edit=}"; printf '%050d' 0; exit 0 ;; esac done printf 'x'; exit 0 @@ -229,6 +241,8 @@ cat > "$TMP/rc/corpus.txt" <<'CORPEOF' . --stub-tmp=$RIPWIRE_CAPSWEEP_TMP . --stub-undefined=$RIPWIRE_CAPSWEEP_NO_SUCH_VAR . --stub-ok --stub-metavar='fn($A, $B, $C)' +. --stub-lose +. --stub-zero CORPEOF rc_out="$TMP/rc.out" # env -u, not `VAR=`: an empty binding is not the operator's normal case, and it used to resolve to the @@ -246,7 +260,7 @@ else # (G) the unbalanced-quote row is UNPARSEABLE, and the rows AFTER it still ran. The second half is # the F1b control: `except ValueError as e` shadows run_corpus's env dict `e`, and Python deletes an # except-name at block end, so the obvious repair kills the NEXT row with UnboundLocalError. - if grep -q 'unparseable' "$rc_out" && grep -Eq '^EXECUTABILITY.*: 4/7 answered' "$rc_out"; then + if grep -q 'unparseable' "$rc_out" && grep -Eq '^EXECUTABILITY.*: 5/9 answered' "$rc_out"; then ok "(G) an unbalanced quote is recorded UNPARSEABLE and the rows after it still run" else no "(G) unparseable row not classified, or the rows after it did not run: $( grep -m1 EXECUTABILITY "$rc_out" )" @@ -258,12 +272,46 @@ else else no "(H) the refusing row was not recorded as a distinct state: $( grep -- '--stub-refuse' "$TMP/rc-screen.tsv" )" fi - # (I) the denominator is the ANSWERING rows: 1 of 3, never 1 of 6. - if grep -q 'cap-sensitive: 1 of 4 answering rows' "$rc_out"; then - ok "(I) the split is reported over the 4 answering rows, not over all 7" + # (I) the denominator is the ANSWERING rows, never every row in the file. + if grep -q 'cap-sensitive: 1 of 5 answering rows' "$rc_out"; then + ok "(I) the split is reported over the 5 answering rows, not over all 9" else no "(I) the split was not reported over the answering rows: $( grep -m1 'cap-sensitive' "$rc_out" )" fi + + # (I/#127-3985249659) THE SPLIT IS OVER EXECUTION STATES. Two rows in the corpus above change STATE + # between the arms and neither is byte sensitivity. The numerator must be 1 — the --stub-cap row — + # and each transition must be named for what it is: + # --stub-lose answered 100 B at the default, refused when bumped → LOST, never counted + # --stub-zero refused at the default, exit 0 / 0 bytes when bumped → NOT an answer (answered()), + # so it is neither cap-sensitive nor a BY-CAP row + # The pre-change spelling `base.get(c) != allb.get(c)` put --stub-lose in the numerator (cap-sensitive + # 2 of 5) and --stub-zero in the BY-CAP list. + if awk -F'\t' '/--stub-lose/ { exit !($3 == "0") }' "$TMP/rc-screen.tsv"; then + ok "(I) a row that answered at the default and STOPPED when bumped is not marked cap-sensitive" + else + no "(I) --stub-lose was marked cap-sensitive — a bump regression counted as byte sensitivity: $( grep -- '--stub-lose' "$TMP/rc-screen.tsv" )" + fi + if awk -F'\t' '/--stub-zero/ { exit !($3 == "0") }' "$TMP/rc-screen.tsv"; then + ok "(I) a row whose bumped arm exits 0 with ZERO bytes is not marked cap-sensitive" + else + no "(I) --stub-zero was marked cap-sensitive — zero bytes counted as an answer: $( grep -- '--stub-zero' "$TMP/rc-screen.tsv" )" + fi + if grep -Eq '^ +LOST .*--stub-lose' "$rc_out"; then + ok "(I) the lost row is REPORTED, with both states, rather than silently dropped" + else + no "(I) --stub-lose was excluded from the ratio AND from the screen — a change nobody is told about" + fi + if grep -Eq '^ +BY-CAP .*--stub-zero' "$rc_out"; then + no "(I) --stub-zero was listed as BY-CAP — a zero-byte exit 0 is not an answer" + else + ok "(I) a zero-byte bumped arm is not reported as a row that 'answers only when a cap is bumped'" + fi + if grep -q 'NUMERATOR = rows where BOTH arms answered' "$TMP/rc-screen.tsv"; then + ok "(I) the records state the state rule the numerator was computed under" + else + no "(I) the screen records do not say that the numerator needs BOTH arms to have answered" + fi if grep -q 'split recipe: DENOMINATOR' "$TMP/rc-screen.tsv"; then ok "(I) the records carry the recipe the ratio was computed by" else @@ -348,6 +396,37 @@ else *) no "(K) the litter run failed for the wrong reason: $( echo "$out" | tail -2 | tr '\n' ' ' )" ;; esac fi +# (K2/#127-3985249656) A NAME LIST CANNOT SEE AN OVERWRITE. The fingerprint carries each entry's type and +# content digest, so a row that rewrites an EXISTING corpus file in place — the same shape that put a +# 10.4 MB cache blob in the frozen tree, on its second run — aborts and names the path with `~`. A +# creation (arm K above) was already caught; this is the half the file list was blind to. +mkdir -p "$TMP/rcedit/src"; printf 'original\n' > "$TMP/rcedit/src/a.h" +cat > "$TMP/rcedit/corpus.txt" <<'CORPEOF' +. --stub-ok +. --stub-edit=src/a.h +CORPEOF +if out="$( python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rcedit" \ + --corpus-file "$TMP/rcedit/corpus.txt" --records "$TMP/rcedit-screen.tsv" 2>&1 )"; then + no "(K2) a row that OVERWROTE a corpus file was measured anyway — the fingerprint is name-only" +else + case "$out" in + *'corpus CHANGED'*'~ src/a.h'*) + ok "(K2) a corpus file overwritten IN PLACE aborts the run and names it as changed, not added" ;; + *) no "(K2) the overwrite run failed for the wrong reason: $( echo "$out" | tail -3 | tr '\n' ' ' )" ;; + esac +fi +# the control: the same corpus with only the non-writing row is measured, so (K2) is not refusing everything +cat > "$TMP/rcedit/corpus.txt" <<'CORPEOF' +. --stub-ok +CORPEOF +printf 'original\n' > "$TMP/rcedit/src/a.h" +if python3 "$GEN" run-corpus --binary "$TMP/stub/ripwire" --corpus "$TMP/rcedit" \ + --corpus-file "$TMP/rcedit/corpus.txt" --records "$TMP/rcedit-screen2.tsv" >/dev/null 2>&1; then + ok "(K2) control: a corpus whose bytes hold still is measured — the digest is not refusing everything" +else + no "(K2) control: a corpus that did not change was refused — the content fingerprint is over-firing" +fi + # and the control: the SAME corpus without the littering row must be measured, or (K) refuses everything cat > "$TMP/rclitter/corpus.txt" <<'CORPEOF' . --stub-ok From 13c461f8f137277836be11aa3d0006813a49aecb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 22:54:16 -0400 Subject: [PATCH 65/73] refactor: the four quality-delta regressions this round's own work introduced MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `--quality-delta` against the branch point (82110b28) gated on four findings, all mine. None acked — the rule is fix your own footprint, and each of these is a real one. complexity src/lintrules.h::findErrorMasking was=18 now=34 bar=15 verbosity src/lintrules.h::findErrorMasking was=47 now=69 bar=60 The raw-bytes confirm went in as a nested block inside the match loop. It is its own step — a one-entry file memo, a bounds check and a scan — so it is now its own function, errorMaskConfirmOnDisk, and the loop reads as one condition again. Same behaviour: the same memo, the same DEGRADED_PATH_ALERT, the same "unreadable or moved file answers false" degrade. verbosity bench/capsweep/capsweep.py::screen_core was=49 now=62 bar=60 The state classification lifted out as classify_split(), which is where its reasoning belongs anyway — screen_core is the phase, not the taxonomy. duplication file_digest | sha tokens=56 (bench/capsweep/capsweep.py:459) The chunk loop I wrote was a token-for-token copy of bench/svectorab.py::sha, which I would have found by running --exemplar/--grep BEFORE writing it. It now uses hashlib's own file_digest (3.11+) with a read-whole fallback — shorter than either copy, and not a third spelling of the same loop. AFTER, same comparison (working tree vs 82110b28): regressions="18" minor="16" preexisting-worse="16" new-symbol="2" gating="0" → exit 0 Re-verified after the refactor: qddialscheck ALL PASS · capsweepcheck ALL PASS · qualitykindscheck ALL PASS · qschemetripcheck ALL PASS BYTE-IDENTICAL on both external corpora (go, canyonraid48): --top-k=100000, --for, --pack-task, --no-cache — six of six. determinism: two --no-cache runs of the map cmp-identical · xmllint --noout clean --test-gate exit 4, naming test/capsweepcheck.sh (run, ALL PASS) plus the pre-existing untested dispatch hubs. --- bench/capsweep/capsweep.py | 55 +++++++++++++++++++--------------- src/lintrules.h | 60 +++++++++++++++++++++----------------- 2 files changed, 65 insertions(+), 50 deletions(-) diff --git a/bench/capsweep/capsweep.py b/bench/capsweep/capsweep.py index fb2aac0f9..77115af0f 100644 --- a/bench/capsweep/capsweep.py +++ b/bench/capsweep/capsweep.py @@ -457,12 +457,13 @@ def assert_no_git_above(corpus): FINGERPRINT = 'corpus.filelist' # lives in --scratch, never in the corpus def file_digest(path): - """sha256 of one file's bytes, read in chunks so a large fixture costs no memory.""" - h = hashlib.sha256() + """sha256 of one file's bytes. hashlib's own chunked reader where the interpreter has it (3.11+), + so this is NOT a third hand-rolled copy of the chunk loop bench/svectorab.py::sha already spells — + --quality-delta flagged exactly that clone when this function first landed as one.""" with open(path, 'rb') as fh: - for chunk in iter(lambda: fh.read(1 << 20), b''): - h.update(chunk) - return h.hexdigest() + if hasattr(hashlib, 'file_digest'): + return hashlib.file_digest(fh, 'sha256').hexdigest() + return hashlib.sha256(fh.read()).hexdigest() def fingerprint_corpus(corpus): """The corpus's file list WITH each entry's type and content digest, `.git/` excluded. @@ -683,6 +684,30 @@ def census(corpus, sizes, states): zero = [c for c in corpus if states.get(c) == kStateOk and (sizes.get(c) or 0) == 0] return ok, unp, unx, to, ref, zero +def classify_split(corpus, base, bstate, allb, gstate, ok): + """The four ways a row can differ between the two arms, by EXECUTION STATE rather than raw value. + + CodeRabbit #127 / 3985249659. `base.get(c)` is None for every non-answer — a timeout, a refusal, an + unparseable row — and 0 for an exit-0 run that printed nothing, which `answered()` already defines as + NO answer. Comparing those values directly counted two things that are not byte sensitivity: + * 100 bytes at the default, TIMEOUT when bumped: base=100, allb=None, "different" -> counted as + cap-sensitive. It is a regression the bump introduced, and it inflated the numerator. + * refused at the default, exit 0 with ZERO bytes when bumped: base=None, allb=0, "different" -> + counted as "answers only when a cap is bumped", about a row that still answers nothing. + + So: hot compares BYTES only where BOTH arms answered; late is a bumped-only answer by answered()'s + own definition; lost answered at the default and stopped; moved never answered in either arm yet the + records differ. Only hot is the numerator; the other three are reported, never counted. + """ + answBase = set(ok) + answBump = set(c for c in corpus if answered(allb, gstate, c)) + hot = sorted(c for c in corpus if c in answBase and c in answBump and base.get(c) != allb.get(c)) + late = sorted(answBump - answBase) + lost = sorted(answBase - answBump) + moved = sorted(c for c in corpus if c not in answBase and c not in answBump + and (bstate.get(c) != gstate.get(c) or base.get(c) != allb.get(c))) + return hot, late, lost, moved + def screen_core(binary, croot, corpus, bump, out_path, measured_at, before): """Both arms, the executability census, the refusal, and the split — over the ANSWERING rows. @@ -716,24 +741,8 @@ def screen_core(binary, croot, corpus, bump, out_path, measured_at, before): ' split. A ratio over a population that measured nothing is not a result.' % len(corpus)) - # THE SPLIT IS OVER EXECUTION STATES, NOT RAW VALUES (CodeRabbit #127 / 3985249659). `base.get(c)` - # is None for every non-answer — a timeout, a refusal, an unparseable row — and 0 for an exit-0 run - # that printed nothing, which `answered()` already defines as NO answer. Comparing those values - # directly counted two things that are not byte sensitivity: - # * 100 bytes at the default, TIMEOUT when bumped: base=100, allb=None, "different" → counted as - # cap-sensitive. It is a regression the bump introduced, and it inflated the numerator. - # * refused at the default, exit 0 with ZERO bytes when bumped: base=None, allb=0, "different" → - # counted as "answers only when a cap is bumped", about a row that still answers nothing. - # So: compare BYTES only where BOTH arms answered, classify a bumped-only answer with answered() - # over the bumped arm, and report every other transition as what it is. - answBase = set(ok) - answBump = set(c for c in corpus if answered(allb, gstate, c)) - hot = sorted(c for c in corpus if c in answBase and c in answBump and base.get(c) != allb.get(c)) - late = sorted(answBump - answBase) # answered ONLY under the bumped arm — real signal - lost = sorted(answBase - answBump) # answered at the DEFAULT and stopped: a bump regression - moved = sorted(c for c in corpus if c not in answBase and c not in answBump - and (bstate.get(c) != gstate.get(c) or base.get(c) != allb.get(c))) - sens = sorted(set(hot) | set(late)) # the rows cmd_sweep will probe cap by cap + hot, late, lost, moved = classify_split(corpus, base, bstate, allb, gstate, ok) + sens = sorted(set(hot) | set(late)) # the rows cmd_sweep will probe cap by cap recipe = ('split recipe: DENOMINATOR = rows that answered under the BASELINE arm (state=ok, >0 bytes).', 'A row that emits nothing cannot respond to a cap; %d row(s) of %d never answer and are' % (len(corpus) - len(ok), len(corpus)), diff --git a/src/lintrules.h b/src/lintrules.h index d01305e6b..88e31535c 100644 --- a/src/lintrules.h +++ b/src/lintrules.h @@ -1197,6 +1197,36 @@ inline bool errorMaskBlockIsEmpty( std::string_view collapsed ) noexcept return errorMaskBlockIsBareBraces( collapsed ) || errorMaskBlockIsCommentOnly( collapsed ); } +// THE CONFIRM (CodeRabbit #127 / 3985249701), as its own step so findErrorMasking stays under the bars. +// The flattened prefilter cannot see where a `//` comment ends, because astQuery scrubbed the newline +// that ended it — so a block admitted through its COMMENT half is re-asked of the file's own bytes. A +// bare `{}` never reaches here: there is no comment there to mis-read, and skipping it keeps the cost at +// "one read per file that has a comment-shaped candidate", a handful of files rather than the corpus. +// +// `m.text.size()` IS the cut length makeAstMatch used (the scrub is byte-for-byte), so the raw slice is +// the same span — the 120-byte floor §Q-DIAL-6 discloses is preserved exactly. `memoFileId`/`memoBytes` +// are the caller's ONE-ENTRY memo: astQuery already sorts (file, startByte, tag), so one slot holds a +// whole file's candidates. An UNREADABLE or MOVED file answers false — a finding that cannot be +// substantiated is not reported. Never throws. +inline bool errorMaskConfirmOnDisk( const IngestResult& ing, const AstMatch& m, + std::uint32_t& memoFileId, std::string& memoBytes ) +{ + if( m.fileId != memoFileId ) + { + memoFileId = m.fileId; + memoBytes.clear(); + if( !docparse::detail::readWholeFile( diskPath( ing, m.fileId ), memoBytes ) ) + { + DEGRADED_PATH_ALERT( "lintrules: error-mask confirm cannot re-read the block's file" ); + } + } + if( std::size_t( m.startByte ) + m.text.size() > memoBytes.size() ) + { + return false; + } + return errorMaskCommentConsumesBlock( std::string_view( memoBytes ).substr( m.startByte, m.text.size() ) ); +} + // One error-masking hit: the suppressing block's file + start byte (so a caller can attribute it to the // enclosing symbol by span containment), the 1-based line, and the rule id. Shaped for span attribution, // not for direct emission — quality.h owns the delta accounting. @@ -1261,34 +1291,10 @@ inline std::vector findErrorMasking( const IngestResult& ing ) { continue; // the @p identifier capture is dropped here too (never "{}") } - // THE CONFIRM (CodeRabbit #127 / 3985249701). The prefilter above cannot see where a `//` comment - // ends, because astQuery scrubbed the newline that ended it — so a block admitted through its - // COMMENT half is re-asked of the file's own bytes. A bare `{}` needs no confirm: there is no - // comment there to mis-read, and skipping it keeps the cost at "one read per file that has a - // comment-shaped candidate", which on this repo's history is a handful of files, not the corpus. - // - // `m.text.size()` IS the cut length makeAstMatch used (the scrub is byte-for-byte), so the raw - // slice is the same span — the 120-byte floor §Q-DIAL-6 discloses is preserved exactly. An - // UNREADABLE file degrades to dropping the row: a finding we cannot substantiate is not reported. - if( rule.emptyOnly && !errorMaskBlockIsBareBraces( m.text ) ) + if( rule.emptyOnly && !errorMaskBlockIsBareBraces( m.text ) + && !errorMaskConfirmOnDisk( ing, m, rawFileId, rawBytes ) ) { - if( m.fileId != rawFileId ) - { - rawFileId = m.fileId; - rawBytes.clear(); - if( !docparse::detail::readWholeFile( diskPath( ing, m.fileId ), rawBytes ) ) - { - DEGRADED_PATH_ALERT( "lintrules: error-mask confirm cannot re-read the block's file" ); - } - } - if( std::size_t( m.startByte ) + m.text.size() > rawBytes.size() ) - { - continue; // the file moved under us, or could not be read — do not assert a swallow - } - if( !errorMaskCommentConsumesBlock( std::string_view( rawBytes ).substr( m.startByte, m.text.size() ) ) ) - { - continue; // a comment OPENS the block but code follows it — that is a handler - } + continue; // a comment OPENS the block but code follows it — that is a handler } out.push_back( { m.fileId, m.startByte, m.line, std::string( rule.id ) } ); } From 5afadeca6e92c7ecd80755d757bc4cb1adeb408f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 23:07:57 -0400 Subject: [PATCH 66/73] test: three pins reconciled after the CodeRabbit lane merged MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs/LIMITS.md regenerated (one row moved with the cache-key field fix); cppqualcheck --uses=readWholeFile 23 -> 24 (quality.h's error-masking confirm now reads a block's raw bytes through the documented reader); mcpcontractcheck's new arm (G/#127) probed situational_awareness on the LIVE working tree — total=0 on a clean checkout, which the arm read as a paging defect (the gate-fixture-is-the-live-repo trap). It now names two source files and carries a presence guard so a zero can never pass it vacuously. --- docs/LIMITS.md | 2 +- test/cppqualcheck.sh | 10 ++++++---- test/mcpcontractcheck.sh | 12 ++++++++++-- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 3777ebbbf..3668dfc56 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -545,7 +545,7 @@ Discloses: **none** ### `src/mcpverbs.h` -Discloses: `coboost_commits_capped`, `hits_capped`, `unindexed_candidates_capped` +Discloses: `blast_radius_capped`, `coboost_commits_capped`, `forgotten_capped`, `hits_capped`, `unindexed_candidates_capped` | constant | value | class | note | | --- | --- | --- | --- | diff --git a/test/cppqualcheck.sh b/test/cppqualcheck.sh index d341e2234..e5dce7306 100755 --- a/test/cppqualcheck.sh +++ b/test/cppqualcheck.sh @@ -180,7 +180,9 @@ US="$( run . --uses=selectBaseline --no-cache )" # (the feat/readability-lens round); 5 -> 6 when src/renamemine.h adopted the same one (feat/naming-calibration); # 6 -> 7 when src/commentcoherence.h adopted the same one (feat/comment-coherence); # 22 -> 23 when src/lexical.h adopted the same one (the 2026-09-10 string-perf round: lexicalScanText read -# every file through ifstream + ostringstream << rdbuf() + str(), two copies, on the BM25 scan path). +# every file through ifstream + ostringstream << rdbuf() + str(), two copies, on the BM25 scan path); +# 23 -> 24 when src/quality.h's error-masking confirm read a block's RAW bytes through it (CodeRabbit on +# #127: a flattened block loses the newline that ends a // comment, so comment-only had to be confirmed raw). # The literal counts REAL call sites, so it moves when a real call site is # added; what it pins is that the qualified `docparse::detail::` spelling still RESOLVES, which is the defect # this arm was written for. Bumping it is correct; changing it to a >= would retire the arm. @@ -213,9 +215,9 @@ US="$( run . --uses=selectBaseline --no-cache )" # 19 -> 22 2026-09-09 (harvest githarden): githarden.h's local-config pre-scan reads the `.git` gitdir FILE, the # gitdir's `commondir`, and each config candidate through the same canonical helper — three sites for one probe, # rather than a fourth fopen/fread of its own. -[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 23 ] \ - && ok "repo: --uses=readWholeFile count=23 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ - || no "repo: --uses=readWholeFile expected 23" +[ "$( cnt "$( run . --uses=readWholeFile --no-cache )" )" = 24 ] \ + && ok "repo: --uses=readWholeFile count=24 (docparse::detail:: — a seam the audit's rw::-anchored grep missed)" \ + || no "repo: --uses=readWholeFile expected 24" [ "$( cnt "$( run . --callers=writeTally --no-cache )" )" = 1 ] \ && ok "repo: --callers=writeTally count=1 (was 0 — both template call sites are in writeDocDriftPage)" \ || no "repo: --callers=writeTally expected 1" diff --git a/test/mcpcontractcheck.sh b/test/mcpcontractcheck.sh index e5b4c977b..64bac6fa5 100755 --- a/test/mcpcontractcheck.sh +++ b/test/mcpcontractcheck.sh @@ -413,13 +413,21 @@ for verb, args, arrayKey in ( ( "find_referencing_symbols", { "path": ROOT, "sym # equal the rows actually served, capped must equal shown < total — so a hand-written constant cannot # satisfy this arm. try: - bare = json.loads( srvG.tool( "situational_awareness", { "path": ROOT } )[ "result" ][ "content" ][ 0 ][ "text" ] ) + # Named files, not the working tree's git diff: on a CLEAN checkout (CI, or an integrator's tree) the + # bare form has zero changed files, zero blast radius, total=0 — and every assertion below reads that + # zero as a red about paging. The fixture must not be the live repo's dirty state (the same trap + # gate-fixture-is-the-live-repo names); src/graph.h + src/verbs_for.h reach well over two files. + SITU_FILES = "src/graph.h,src/verbs_for.h" + bare = json.loads( srvG.tool( "situational_awareness", { "path": ROOT, "files": SITU_FILES } )[ "result" ][ "content" ][ 0 ][ "text" ] ) cut = json.loads( srvG.tool( "situational_awareness", - { "path": ROOT, "limit": 2, "offset": 0 } )[ "result" ][ "content" ][ 0 ][ "text" ] ) + { "path": ROOT, "files": SITU_FILES, "limit": 2, "offset": 0 } )[ "result" ][ "content" ][ 0 ][ "text" ] ) except Exception as e: check( False, "(G) situational_awareness paging probe failed: %s" % e ) bare = cut = None if bare is not None: + check( len( bare.get( "blast_radius", [] ) ) > 2, + "(G) presence guard: the named files reach %d blast-radius rows (> the 2-row window; a zero here would make every arm below vacuous)" + % len( bare.get( "blast_radius", [] ) ) ) for doc, label in ( ( bare, "bare" ), ( cut, "limit=2" ) ): for arr, shownKey, cappedKey in ( ( "blast_radius", "shown_blast_radius", "blast_radius_capped" ), ( "forgotten", "shown_forgotten", "forgotten_capped" ) ): From 64fcd72477b8d9dec828eb6b8fa18529a3737c49 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Thu, 10 Sep 2026 23:33:44 -0400 Subject: [PATCH 67/73] =?UTF-8?q?test(fieldidcheck):=20the=20harness=20lin?= =?UTF-8?q?ks=20grammar=20objects=20it=20compiled=20itself=20=E2=80=94=20f?= =?UTF-8?q?lavour-independent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 2 of PR #127 still failed this gate on BOTH ubuntu Release legs (gcc and clang): the build's grammar objects and libtree-sitter.a are LTO bitcode/GIMPLE under RIPWIRE_LTO (implied by Release), a plain link cannot read them on Linux, and the -flto retry from run 1 did not close it either — while every plain leg and every macOS leg (whose linker reads bitcode transparently) passed. The gate now compiles what it links: the same vendored sources the build compiled (discovered from the build's own ts_*.dir object list, so the grammar set cannot drift from CMake's) plus lib/src/lib.c, at -O1, once per run (41 sources + core, ~10 s), and links the harness against those. 14/14 arms on a plain build and on a Release build locally. --- test/fieldidcheck.sh | 46 ++++++++++++++++++++++++++++++-------------- 1 file changed, 32 insertions(+), 14 deletions(-) diff --git a/test/fieldidcheck.sh b/test/fieldidcheck.sh index eee0a0b67..7619f2ca8 100755 --- a/test/fieldidcheck.sh +++ b/test/fieldidcheck.sh @@ -45,7 +45,7 @@ # fit is correct but silently un-warmed, so the arm is the alarm for the day a 65th grammar lands. # # Usage: bash test/fieldidcheck.sh [ CXX=clang++ ] [ RIPWIRE_BIN=build/ripwire ] -# RIPWIRE_BIN is used ONLY to locate the build directory holding the compiled grammar objects (the vendored +# RIPWIRE_BIN is used ONLY to locate the build directory whose grammar OBJECT LIST names the sources to compile (the vendored # grammars and the tree-sitter core the harness links against) — this gate never EXECUTES the ripwire # binary. It still needs no pin in test/binoverridecheck.sh's EXEMPT dict: a sentinel RIPWIRE_BIN points at # a directory with no grammar objects in it, so the gate goes red rather than silently green. @@ -83,6 +83,35 @@ if [ -z "$GRAMMAR_OBJS" ]; then fi echo "fieldidcheck: CXX=$CXX header=src/infra/fieldid.h build=$BUILDDIR" +# ── the harness's own grammar objects, compiled here from the vendored sources ───────────────────────── +# The build's grammar objects and libtree-sitter.a are NOT linkable from a plain command on every flavour: +# a Release build (RIPWIRE_LTO implied ON) leaves them as LTO bitcode/GIMPLE, and both ubuntu Release legs +# of PR #127 (gcc AND clang) failed the plain link while every plain leg and every macOS leg (whose linker +# reads bitcode transparently) passed; a `-flto` retry did not close it either. So the gate compiles what +# it links: the SAME sources the build compiled — discovered from the build's own object list, so the +# grammar set cannot drift from CMake's — plus the core's lib.c, at -O1, into $TMP/gobj. ~20 s, once. +CC="${CC:-cc}" +mkdir -p "$TMP/gobj" +TSCORE="$ROOT/third_party/deps/tree_sitter" +"$CC" -O1 -c "$TSCORE/lib/src/lib.c" -I "$TSCORE/lib/include" -I "$TSCORE/lib/src" -o "$TMP/gobj/ts_core.o" 2>"$TMP/gobj/core.log" \ + || { echo " no self-built tree-sitter core: $( head -3 "$TMP/gobj/core.log" )"; exit 2; } +n_g=0 +for obj in $GRAMMAR_OBJS; do + rel="${obj#*/CMakeFiles/}"; rel="${rel#*.dir/}"; rel="${rel%.o}" # ts_cpp.dir/third_party/deps/cpp/src/parser.c.o → third_party/deps/cpp/src/parser.c + src="$ROOT/$rel"; [ -f "$src" ] || { echo " grammar source missing for $obj: $src"; exit 2; } + name="$( printf '%s' "$rel" | tr '/' '_' )" + case "$src" in + *.cc|*.cpp) "$CXX" "$CXXSTD" -O1 -c "$src" -I "$TSCORE/lib/include" -I "$( dirname "$src" )" -o "$TMP/gobj/$name.o" 2>"$TMP/gobj/$name.log" & ;; + *) "$CC" -O1 -c "$src" -I "$TSCORE/lib/include" -I "$( dirname "$src" )" -o "$TMP/gobj/$name.o" 2>"$TMP/gobj/$name.log" & ;; + esac + n_g=$(( n_g + 1 )) + [ $(( n_g % 6 )) -eq 0 ] && wait +done +wait +n_o="$( ls "$TMP"/gobj/*.o 2>/dev/null | wc -l | tr -d ' ' )" +[ "$n_o" -eq $(( n_g + 1 )) ] || { echo " self-built grammar objects: $n_o of $(( n_g + 1 )) — $( cat "$TMP"/gobj/*.log | head -5 )"; exit 2; } +echo " INFO $n_g grammar source(s) + the core compiled once for the harness ($n_o objects, flavour-independent)" + # ── harvest the field spellings FROM A PRISTINE HEADER ─────────────────────────────────────────────── # Enumerator order and the { "spelling", len } rows, read as TEXT. The harness's reference side uses # these literals, which is what lets arm D mutate the header under test without moving the reference. @@ -360,20 +389,9 @@ int main( int argc, char** argv ) } """ ) PYHARNESS - # A Release build (RIPWIRE_LTO implied ON) leaves the grammar objects and libtree-sitter.a as LTO - # bitcode/GIMPLE, which a plain link cannot read on Linux ("plugin needed to handle lto object" / - # undefined tree_sitter_* references); macOS's linker reads them transparently, which is why this gate - # was green on every macOS leg and red on both ubuntu Release legs of PR #127's first run. Link plainly - # first (the plain build's objects), and retry the same command with -flto when that fails. - if "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra \ - -I "$incdir" -I "$ROOT/third_party/deps/tree_sitter/lib/include" \ - "$TMP/harness.cpp" $GRAMMAR_OBJS "$TSLIB" -o "$out" 2>"$log"; then - return 0 - fi - cp "$log" "$log.plain" - "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra -flto \ + "$CXX" "$CXXSTD" -O1 -g -Wall -Wextra \ -I "$incdir" -I "$ROOT/third_party/deps/tree_sitter/lib/include" \ - "$TMP/harness.cpp" $GRAMMAR_OBJS "$TSLIB" -o "$out" 2>"$log" + "$TMP/harness.cpp" "$TMP"/gobj/*.o -o "$out" 2>"$log" } # ── A0/A/B/C against the real header ───────────────────────────────────────────────────────────────── From 5c808487b66195a2c310e191bf8030ed5747a6eb Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 00:06:07 -0400 Subject: [PATCH 68/73] =?UTF-8?q?test(strkerncheck):=20two=20vacuous-pass?= =?UTF-8?q?=20shapes=20closed=20=E2=80=94=20the=20zero=20count=20that=20pr?= =?UTF-8?q?inted=20twice,=20and=20a=20failing=20slice=20read=20as=20SKIP?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CodeRabbit on #127@64fcd724, both valid, both red-first: - `grep -c … || echo 0` yields "0\\n0" on a zero count (grep prints 0 AND exits 1), a value `-lt` cannot compare, so the portability arm could PASS on the count it exists to refuse. Now `grep -o | wc -l`. - Arms 3 and 3b classified EVERY nonzero exit of the x86_64 slice as "no Rosetta 2" (SKIP). Only a cross-architecture exec failure (rc 126 / Bad CPU type / cannot execute binary file / Exec format error) is a SKIP now; a slice that RAN and exited nonzero is a FAIL. Control arm 3c runs the x86_64 build of the mutation and requires it to read as a red (it does: rc=1). --- test/strkerncheck.sh | 43 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 37 insertions(+), 6 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 7f44d219f..2c9217101 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -140,7 +140,9 @@ fi HDR="$ROOT/src/infra/strkern.h" code_hits(){ grep -n "$1" "$HDR" 2>/dev/null | grep -vE '^[0-9]+: *(//|\*|/\*)'; } BUILTINS="$( code_hits '__builtin_' | wc -l | tr -d ' ' )" -CTZ="$( grep -c 'std::countr_zero(' "$HDR" 2>/dev/null || echo 0 )" +# `grep -c … || echo 0` printed "0" TWICE on a zero count (grep prints 0 AND exits 1), a two-line value +# `-lt` cannot compare — so the arm could PASS on the very count it exists to refuse (CodeRabbit on #127). +CTZ="$( grep -o 'std::countr_zero(' "$HDR" 2>/dev/null | wc -l | tr -d ' ' )" if [ "$BUILTINS" != "0" ]; then echo " FAIL portability: src/infra/strkern.h uses $BUILTINS GCC/Clang-only __builtin_ — MSVC cannot compile it:" code_hits '__builtin_' | sed 's/^/ /' | head -10 @@ -214,6 +216,14 @@ else fail=1 fi +# A cross-arch slice that Rosetta 2 cannot run fails at EXEC (rc 126, "Bad CPU type in executable", +# "cannot execute binary file", "Exec format error"); that — and only that — is the environment saying +# no. A slice that RAN and exited nonzero (a doctest assertion, an abort) is a red, never a SKIP +# (CodeRabbit on #127: the old branch read every nonzero exit as "no Rosetta"). +exec_unavailable(){ # $1 = rc, $2 = output log + [ "$1" = 126 ] || grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" +} + # ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── # The x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE; CMakeLists.txt sets it # unconditionally for x86-64 targets). Compiled without sanitizers — the ASan runtime for a cross-arch @@ -221,7 +231,8 @@ fi # memory safety arm 1 already did. if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then if X86BIN="$( compile_direct x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then - if RIPWIRE_ROOT="$ROOT" "$X86BIN" > "$WORK/out_x86.log" 2>&1; then + RIPWIRE_ROOT="$ROOT" "$X86BIN" > "$WORK/out_x86.log" 2>&1; rc_x86=$? + if [ "$rc_x86" = 0 ]; then read_counts "$WORK/out_x86.log" if grep -q '^strkern path: AVX2$' "$WORK/out_x86.log"; then printf ' PASS x86_64/AVX2 mirror runs green under Rosetta 2 (%s assertions)\n' "$ASSERTS" @@ -229,9 +240,12 @@ if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then echo " FAIL x86_64 slice built but did NOT compile the AVX2 path: $( grep '^strkern path: ' "$WORK/out_x86.log" )" fail=1 fi + elif exec_unavailable "$rc_x86" "$WORK/out_x86.log"; then + printf ' SKIP x86_64 slice built but cannot execute here (no Rosetta 2); CI ubuntu-24.04 is the AVX2 proof: %s\n' \ + "$( tail -1 "$WORK/out_x86.log" )" else - printf ' SKIP x86_64 slice built but did not run here (no Rosetta 2, or it aborted); CI ubuntu-24.04 is the AVX2 proof: %s\n' \ - "$( tail -2 "$WORK/out_x86.log" | tr '\n' ' ' )" + echo " FAIL x86_64/AVX2 mirror RAN and exited $rc_x86: $( tail -2 "$WORK/out_x86.log" | tr '\n' ' ' )" + fail=1 fi else printf ' SKIP no x86_64 cross slice on this toolchain (no macOS x86_64 SDK); CI ubuntu-24.04 is the AVX2 proof\n' @@ -248,14 +262,31 @@ fi # all (no Rosetta 2) is a SKIP, as in arm 3. if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then if X86UB="$( compile_direct x86ub -arch x86_64 -march=x86-64-v3 -fsanitize=undefined,integer -fno-sanitize-recover=all )" && [ -n "$X86UB" ]; then - if RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; then + RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; rc_ub=$? + if [ "$rc_ub" = 0 ]; then read_counts "$WORK/out_x86ub.log" printf ' PASS x86_64/AVX2 mirror is clean under -fsanitize=undefined,integer (%s assertions)\n' "$ASSERTS" elif grep -q 'runtime error' "$WORK/out_x86ub.log"; then echo " FAIL x86_64/AVX2 mirror trips UBSan integer checks: $( grep -m1 'runtime error' "$WORK/out_x86ub.log" | sed 's|.*/src/|src/|' )" fail=1 + elif exec_unavailable "$rc_ub" "$WORK/out_x86ub.log"; then + printf ' SKIP x86_64 UBSan slice built but cannot execute here (no Rosetta 2): %s\n' "$( tail -1 "$WORK/out_x86ub.log" )" else - printf ' SKIP x86_64 UBSan slice built but did not run here (no Rosetta 2): %s\n' "$( tail -1 "$WORK/out_x86ub.log" )" + echo " FAIL x86_64 UBSan slice RAN and exited $rc_ub without a sanitizer report: $( tail -2 "$WORK/out_x86ub.log" | tr '\n' ' ' )" + fail=1 + fi + # 3c CONTROL — a slice that runs and FAILS must read as FAIL, never as "no Rosetta": the x86_64 build + # of the mutation (arm 2's -DSTRKERN_MUTATE=1) is exactly that binary. + if X86MUT="$( compile_direct x86mut -arch x86_64 -march=x86-64-v3 -DSTRKERN_MUTATE=1 )" && [ -n "$X86MUT" ]; then + RIPWIRE_ROOT="$ROOT" "$X86MUT" > "$WORK/out_x86mut.log" 2>&1; rc_mut=$? + if [ "$rc_mut" != 0 ] && ! exec_unavailable "$rc_mut" "$WORK/out_x86mut.log"; then + echo " PASS 3c control: the mutated x86_64 slice RAN and failed (rc=$rc_mut) — a red, classified as a red, not a SKIP" + elif exec_unavailable "$rc_mut" "$WORK/out_x86mut.log"; then + echo " SKIP 3c control: the mutated x86_64 slice cannot execute here either (no Rosetta 2)" + else + echo " FAIL 3c control: the mutated x86_64 slice exited 0 — the mutation is not visible on the AVX2 path" + fail=1 + fi fi else printf ' SKIP no x86_64 UBSan cross slice on this toolchain; CI ubuntu-24.04 asan is the proof\n' From d6e378ac842f4b65a7f4e4a0bc24254845008142 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 00:46:38 -0400 Subject: [PATCH 69/73] test(strkerncheck): a SIGILL before the slice's first line is Rosetta 2 without AVX2, not a red MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 4 of PR #127 turned both macOS legs red on the arm that run 3's fix had just tightened: the x86_64 slice RAN under Rosetta 2 on the macos-14 runners and exited 132 (SIGILL) with no output. Rosetta 2 gained AVX2 in macOS 15; on macOS 14 a -march=x86-64-v3 slice is illegal at its first vector instruction. That is the emulator lacking the ISA — the same class as 'cannot execute binary file' — so it is a SKIP with the reason printed; a SIGILL after the slice has printed anything remains a FAIL. The run-3 tightening stands: every other nonzero exit of a slice that ran is still a red (control arm 3c). --- test/strkerncheck.sh | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 2c9217101..aebed1047 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -220,8 +220,19 @@ fi # "cannot execute binary file", "Exec format error"); that — and only that — is the environment saying # no. A slice that RAN and exited nonzero (a doctest assertion, an abort) is a red, never a SKIP # (CodeRabbit on #127: the old branch read every nonzero exit as "no Rosetta"). +# A second environmental shape, seen on CI's macos-14 runners (PR #127 run 4): the slice DID execute under +# Rosetta 2 and died with SIGILL (rc 132) before printing its first line — Rosetta 2 gained AVX2 only in +# macOS 15, so a -march=x86-64-v3 slice on macOS 14 is illegal at its first vector instruction. That is +# the emulator lacking the ISA, not a kernel defect: SKIP, with the reason. A SIGILL AFTER the slice has +# printed (its path line, an assertion) is a real red and stays one. exec_unavailable(){ # $1 = rc, $2 = output log - [ "$1" = 126 ] || grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" + [ "$1" = 126 ] && return 0 + grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" && return 0 + if [ "$1" = 132 ] && ! grep -q 'strkern path' "$2"; then + echo " (SIGILL before the first line: this Rosetta 2 has no AVX2 — macOS 15+ runs the v3 slice; macOS 14 cannot)" + return 0 + fi + return 1 } # ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── From d94ac2842a6f8b448d427094d116815fb086e60f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 00:58:57 -0400 Subject: [PATCH 70/73] test(strkerncheck): the Rosetta arms run only where a probe has EXECUTED an AVX2 instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 4 of PR #127: CI's macos-14 runners SIGILL'd the -march=x86-64-v3 slice at its first vector instruction (Rosetta 2 gained AVX2 in macOS 15), and control 3c 'passed' on that SIGILL (rc 132) — it was seeing the emulator, not the mutation (#128's lane and the coordinator spotted it). The sysctl probes cannot decide this: on a macOS 26 host whose Rosetta runs the v3 slice green, hw.optional.avx2_0 still reads 0 and leaf7_features lists no AVX2. So the gate compiles and RUNS a one-instruction AVX2 probe under Rosetta first: rc 0 + 'avx2 ok' => arms 3/3b/3c run and every nonzero exit of a slice (SIGILL included) is a FAIL; anything else => the three arms SKIP as rosetta_no_avx2 with the probe's exit printed. Control 3c now requires the mutation's OWN failure — rc != 0, rc != 132, and assertion output — so a SIGILL can never satisfy it. Verified locally in both states (probe available: 3/3b/3c PASS with rc=1 on the mutation; probe forced unavailable: the three arms SKIP, the gate passes on its six host arms). --- test/strkerncheck.sh | 64 +++++++++++++++++++++++++++++++------------- 1 file changed, 45 insertions(+), 19 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index aebed1047..fefa06263 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -152,7 +152,7 @@ elif [ "$CTZ" -lt 8 ]; then echo " counts (2 scalar twins + 6 vector) are the population this arm is non-vacuous over" fail=1 else - printf ' PASS portability: 0 __builtin_ in strkern.h, %s std::countr_zero( sites (MSVC-compilable; included)\n' "$CTZ" + printf ' PASS portability: 0 __builtin_ in strkern.h, %s std::countr_zero( sites (, one spelling for the repo)\n' "$CTZ" fi if ! grep -q '^#include ' "$HDR"; then echo " FAIL portability: strkern.h calls std::countr_zero without including " @@ -220,19 +220,41 @@ fi # "cannot execute binary file", "Exec format error"); that — and only that — is the environment saying # no. A slice that RAN and exited nonzero (a doctest assertion, an abort) is a red, never a SKIP # (CodeRabbit on #127: the old branch read every nonzero exit as "no Rosetta"). -# A second environmental shape, seen on CI's macos-14 runners (PR #127 run 4): the slice DID execute under -# Rosetta 2 and died with SIGILL (rc 132) before printing its first line — Rosetta 2 gained AVX2 only in -# macOS 15, so a -march=x86-64-v3 slice on macOS 14 is illegal at its first vector instruction. That is -# the emulator lacking the ISA, not a kernel defect: SKIP, with the reason. A SIGILL AFTER the slice has -# printed (its path line, an assertion) is a real red and stays one. -exec_unavailable(){ # $1 = rc, $2 = output log - [ "$1" = 126 ] && return 0 - grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" && return 0 - if [ "$1" = 132 ] && ! grep -q 'strkern path' "$2"; then - echo " (SIGILL before the first line: this Rosetta 2 has no AVX2 — macOS 15+ runs the v3 slice; macOS 14 cannot)" - return 0 +# Can the TRANSLATED x86_64 runtime execute AVX2 at all? Rosetta 2 gained AVX2 in macOS 15; CI's macos-14 +# runners SIGILL a -march=x86-64-v3 slice at its first vector instruction (PR #127 run 4, rc 132, no output). +# The sysctl probes are NOT trustworthy here: on a macOS 26 host whose Rosetta runs the v3 slice green, +# `arch -x86_64 sysctl -n hw.optional.avx2_0` still prints 0 and leaf7_features lists no AVX2 — so the probe +# EXECUTES one AVX2 instruction under Rosetta and reads the exit. rc 0 + "avx2 ok" => available; rc 132 +# (SIGILL) or an exec failure => rosetta_no_avx2, and arms 3/3b/3c SKIP with that reason. Where the probe +# runs, a slice that exits nonzero — SIGILL included — is a FAIL, never a SKIP. +ROSETTA_AVX2="unknown" +rosetta_avx2_probe(){ + cat > "$WORK/avx2probe.c" <<'EOF_PROBE' +#include +#include +int main( void ) +{ + volatile int seed = 3; + __m256i a = _mm256_set1_epi8( (char) seed ); + __m256i b = _mm256_add_epi8( a, a ); + unsigned char out[ 32 ]; + _mm256_storeu_si256( (__m256i*) out, b ); + printf( "avx2 ok %d\n", out[ 0 ] ); + return out[ 0 ] == 6 ? 0 : 1; +} +EOF_PROBE + if ! "${CC:-cc}" -arch x86_64 -mavx2 -O1 "$WORK/avx2probe.c" -o "$WORK/avx2probe" 2>"$WORK/avx2probe.cc.log"; then + ROSETTA_AVX2="no_toolchain"; return 1 + fi + "$WORK/avx2probe" > "$WORK/avx2probe.out" 2>&1; local rc=$? + if [ "$rc" = 0 ] && grep -q '^avx2 ok' "$WORK/avx2probe.out"; then + ROSETTA_AVX2="yes"; return 0 fi - return 1 + ROSETTA_AVX2="no (probe rc=$rc: $( tail -1 "$WORK/avx2probe.out" 2>/dev/null | tr -d '\n' ))"; return 1 +} +exec_unavailable(){ # $1 = rc, $2 = output log — the exec-format shapes only; AVX2 absence is decided by the probe above + [ "$1" = 126 ] && return 0 + grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" } # ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── @@ -241,7 +263,10 @@ exec_unavailable(){ # $1 = rc, $2 = output log # slice is not reliably present, and this arm's job is to run the AVX2 kernels at all, not to re-prove # memory safety arm 1 already did. if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then - if X86BIN="$( compile_direct x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then + rosetta_avx2_probe || true + if [ "$ROSETTA_AVX2" != "yes" ]; then + printf ' SKIP x86_64/AVX2 mirror arms 3, 3b, 3c: rosetta_no_avx2 — the translated runtime cannot execute AVX2 here (%s); CI ubuntu-24.04 runs the v3 slice natively and is the proof\n' "$ROSETTA_AVX2" + elif X86BIN="$( compile_direct x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then RIPWIRE_ROOT="$ROOT" "$X86BIN" > "$WORK/out_x86.log" 2>&1; rc_x86=$? if [ "$rc_x86" = 0 ]; then read_counts "$WORK/out_x86.log" @@ -271,7 +296,7 @@ fi # in the Apple toolchain, so the cross slice CAN carry -fsanitize=undefined,integer; ASan stays off here # (arm 1 owns memory safety on the host ISA). A sanitizer report is a FAIL; a slice that will not run at # all (no Rosetta 2) is a SKIP, as in arm 3. -if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then +if { [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; } && [ "$ROSETTA_AVX2" = "yes" ]; then if X86UB="$( compile_direct x86ub -arch x86_64 -march=x86-64-v3 -fsanitize=undefined,integer -fno-sanitize-recover=all )" && [ -n "$X86UB" ]; then RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; rc_ub=$? if [ "$rc_ub" = 0 ]; then @@ -290,10 +315,11 @@ if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then # of the mutation (arm 2's -DSTRKERN_MUTATE=1) is exactly that binary. if X86MUT="$( compile_direct x86mut -arch x86_64 -march=x86-64-v3 -DSTRKERN_MUTATE=1 )" && [ -n "$X86MUT" ]; then RIPWIRE_ROOT="$ROOT" "$X86MUT" > "$WORK/out_x86mut.log" 2>&1; rc_mut=$? - if [ "$rc_mut" != 0 ] && ! exec_unavailable "$rc_mut" "$WORK/out_x86mut.log"; then - echo " PASS 3c control: the mutated x86_64 slice RAN and failed (rc=$rc_mut) — a red, classified as a red, not a SKIP" - elif exec_unavailable "$rc_mut" "$WORK/out_x86mut.log"; then - echo " SKIP 3c control: the mutated x86_64 slice cannot execute here either (no Rosetta 2)" + if [ "$rc_mut" != 0 ] && [ "$rc_mut" != 132 ] && grep -qE 'FAILED|assertion|CHECK' "$WORK/out_x86mut.log"; then + echo " PASS 3c control: the mutated x86_64 slice RAN and failed on its own assertions (rc=$rc_mut) — a red, classified as a red" + elif [ "$rc_mut" = 132 ]; then + echo " FAIL 3c control: the mutated x86_64 slice died with SIGILL although the AVX2 probe ran — that is a real red, not the mutation" + fail=1 else echo " FAIL 3c control: the mutated x86_64 slice exited 0 — the mutation is not visible on the AVX2 path" fail=1 From 7bed6f34c520be64d64894bb0d0ba5499c6eb5c7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 01:12:42 -0400 Subject: [PATCH 71/73] test(strkerncheck): the x86_64 mirror runs the baseline (scalar) slice where Rosetta 2 has no AVX2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner (2026-09-11): a non-AVX2 build of the mirror is fine for the hosts that cannot run v3. strkern.h has no SSE2 path, so on a host whose AVX2 execution probe fails the three mirror arms compile the x86-64 BASELINE slice instead of skipping — the scalar twins and the tokenizer on x86, under the same 26 assertions, the same -fsanitize=undefined,integer, and the same mutation control — and the arms name which slice ran. CI's macos-14 legs (Rosetta 2 without AVX2) therefore keep real x86 kernel coverage; the ubuntu legs run the v3 slice natively; the release floor (-march=x86-64-v3) does not move. Verified locally in both states: probe available → v3 slice, path=AVX2, mutation rc=1; probe forced off → baseline slice, path=scalar, mutation rc=1. A slice that RAN and exited nonzero is a FAIL in every arm; only an exec-format failure skips. --- test/strkerncheck.sh | 95 +++++++++++++++++++++++--------------------- 1 file changed, 49 insertions(+), 46 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index fefa06263..cc8045c5f 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -257,76 +257,79 @@ exec_unavailable(){ # $1 = rc, $2 = output log — the exec-format shapes only grep -qE 'Bad CPU type|cannot execute binary file|Exec format error' "$2" } -# ── 3: best-effort x86_64 / AVX2 mirror under Rosetta 2 ─────────────────────────────────────────────── -# The x86-64 floor is -march=x86-64-v3 (AVX2 + BMI1/2 + FMA + LZCNT + MOVBE; CMakeLists.txt sets it -# unconditionally for x86-64 targets). Compiled without sanitizers — the ASan runtime for a cross-arch -# slice is not reliably present, and this arm's job is to run the AVX2 kernels at all, not to re-prove -# memory safety arm 1 already did. +# ── 3 / 3b / 3c: the x86_64 mirror under Rosetta 2 — v3 (AVX2) where the translated runtime can run it, +# the x86-64 BASELINE (the scalar twins on x86) where it cannot ──────────────────────────────────────── +# The shipped x86-64 floor is -march=x86-64-v3 (CMakeLists.txt sets it unconditionally for x86-64 +# targets). CI's ubuntu legs run that slice natively; on an arm64 Mac it runs under Rosetta 2, which gained +# AVX2 only in macOS 15 — so the probe above decides WHICH slice this host can execute. Owner (2026-09-11): +# a non-AVX2 build of the mirror is fine for the no-AVX2 hosts — strkern.h has no SSE2 path, so that slice +# runs the SCALAR twins and the tokenizer on x86, under the same assertions, sanitizer and mutation control. +# The release floor does not move; this is the test slice only. +# 3 — the slice runs green and compiled the expected path (AVX2 or scalar); +# 3b — the same slice under -fsanitize=undefined,integer (UBSan's runtime is a universal dylib; ASan stays +# off here — arm 1 owns memory safety on the host ISA); a sanitizer report is a FAIL; +# 3c — CONTROL: the slice built with -DSTRKERN_MUTATE=1 must fail on its OWN assertions (rc != 0, +# rc != 132, assertion output) — a SIGILL can never satisfy it. +# A slice that RAN and exited nonzero is a FAIL, never a SKIP; only an exec-format failure SKIPs. if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then rosetta_avx2_probe || true - if [ "$ROSETTA_AVX2" != "yes" ]; then - printf ' SKIP x86_64/AVX2 mirror arms 3, 3b, 3c: rosetta_no_avx2 — the translated runtime cannot execute AVX2 here (%s); CI ubuntu-24.04 runs the v3 slice natively and is the proof\n' "$ROSETTA_AVX2" - elif X86BIN="$( compile_direct x86 -arch x86_64 -march=x86-64-v3 )" && [ -n "$X86BIN" ]; then + if [ "$ROSETTA_AVX2" = "yes" ]; then + X86_MARCH="-march=x86-64-v3"; X86_PATH="AVX2"; X86_LABEL="x86_64/AVX2 (v3) mirror" + else + X86_MARCH="-march=x86-64"; X86_PATH="scalar"; X86_LABEL="x86_64 baseline (scalar) mirror" + printf ' INFO rosetta_no_avx2 — the translated runtime cannot execute AVX2 here (%s); the mirror runs the x86-64 baseline slice (scalar twins on x86); CI ubuntu-24.04 runs the v3 slice natively\n' "$ROSETTA_AVX2" + fi + if X86BIN="$( compile_direct x86 -arch x86_64 $X86_MARCH )" && [ -n "$X86BIN" ]; then RIPWIRE_ROOT="$ROOT" "$X86BIN" > "$WORK/out_x86.log" 2>&1; rc_x86=$? if [ "$rc_x86" = 0 ]; then read_counts "$WORK/out_x86.log" - if grep -q '^strkern path: AVX2$' "$WORK/out_x86.log"; then - printf ' PASS x86_64/AVX2 mirror runs green under Rosetta 2 (%s assertions)\n' "$ASSERTS" + if grep -q "^strkern path: $X86_PATH\$" "$WORK/out_x86.log"; then + printf ' PASS 3: %s runs green under Rosetta 2 (%s assertions, path=%s)\n' "$X86_LABEL" "$ASSERTS" "$X86_PATH" else - echo " FAIL x86_64 slice built but did NOT compile the AVX2 path: $( grep '^strkern path: ' "$WORK/out_x86.log" )" + echo " FAIL 3: $X86_LABEL built but compiled a different path: $( grep '^strkern path: ' "$WORK/out_x86.log" ) (expected $X86_PATH)" fail=1 fi elif exec_unavailable "$rc_x86" "$WORK/out_x86.log"; then - printf ' SKIP x86_64 slice built but cannot execute here (no Rosetta 2); CI ubuntu-24.04 is the AVX2 proof: %s\n' \ - "$( tail -1 "$WORK/out_x86.log" )" + printf ' SKIP 3: %s built but cannot execute here (no Rosetta 2): %s\n' "$X86_LABEL" "$( tail -1 "$WORK/out_x86.log" )" else - echo " FAIL x86_64/AVX2 mirror RAN and exited $rc_x86: $( tail -2 "$WORK/out_x86.log" | tr '\n' ' ' )" + echo " FAIL 3: $X86_LABEL RAN and exited $rc_x86: $( tail -2 "$WORK/out_x86.log" | tr '\n' ' ' )" fail=1 fi - else - printf ' SKIP no x86_64 cross slice on this toolchain (no macOS x86_64 SDK); CI ubuntu-24.04 is the AVX2 proof\n' - fi -fi - -# ── 3b: the x86_64 slice under UBSan's integer checks — the arm that CI's ubuntu ASan leg is ──────────── -# The 32-byte AVX2 block fills every bit of a uint32 mask, so a `<< 1` that is harmless on a 16-byte NEON -# mask (top half always zero) DROPS a set bit on AVX2, and -fsanitize=integer's unsigned-shift-base check -# aborts on exactly that (PR #127's first CI run: lexindex.h:186 on --for/--pack-task, clean on every arm64 -# ASan run). Arm 3 compiled without sanitizers and could not see it. UBSan's runtime is a universal dylib -# in the Apple toolchain, so the cross slice CAN carry -fsanitize=undefined,integer; ASan stays off here -# (arm 1 owns memory safety on the host ISA). A sanitizer report is a FAIL; a slice that will not run at -# all (no Rosetta 2) is a SKIP, as in arm 3. -if { [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; } && [ "$ROSETTA_AVX2" = "yes" ]; then - if X86UB="$( compile_direct x86ub -arch x86_64 -march=x86-64-v3 -fsanitize=undefined,integer -fno-sanitize-recover=all )" && [ -n "$X86UB" ]; then - RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; rc_ub=$? - if [ "$rc_ub" = 0 ]; then - read_counts "$WORK/out_x86ub.log" - printf ' PASS x86_64/AVX2 mirror is clean under -fsanitize=undefined,integer (%s assertions)\n' "$ASSERTS" - elif grep -q 'runtime error' "$WORK/out_x86ub.log"; then - echo " FAIL x86_64/AVX2 mirror trips UBSan integer checks: $( grep -m1 'runtime error' "$WORK/out_x86ub.log" | sed 's|.*/src/|src/|' )" - fail=1 - elif exec_unavailable "$rc_ub" "$WORK/out_x86ub.log"; then - printf ' SKIP x86_64 UBSan slice built but cannot execute here (no Rosetta 2): %s\n' "$( tail -1 "$WORK/out_x86ub.log" )" + if X86UB="$( compile_direct x86ub -arch x86_64 $X86_MARCH -fsanitize=undefined,integer -fno-sanitize-recover=all )" && [ -n "$X86UB" ]; then + RIPWIRE_ROOT="$ROOT" UBSAN_OPTIONS=halt_on_error=1:print_stacktrace=1 "$X86UB" > "$WORK/out_x86ub.log" 2>&1; rc_ub=$? + if [ "$rc_ub" = 0 ]; then + read_counts "$WORK/out_x86ub.log" + printf ' PASS 3b: %s is clean under -fsanitize=undefined,integer (%s assertions)\n' "$X86_LABEL" "$ASSERTS" + elif grep -q 'runtime error' "$WORK/out_x86ub.log"; then + echo " FAIL 3b: $X86_LABEL trips UBSan integer checks: $( grep -m1 'runtime error' "$WORK/out_x86ub.log" | sed 's|.*/src/|src/|' )" + fail=1 + elif exec_unavailable "$rc_ub" "$WORK/out_x86ub.log"; then + printf ' SKIP 3b: %s UBSan slice cannot execute here (no Rosetta 2): %s\n' "$X86_LABEL" "$( tail -1 "$WORK/out_x86ub.log" )" + else + echo " FAIL 3b: $X86_LABEL UBSan slice RAN and exited $rc_ub without a sanitizer report: $( tail -2 "$WORK/out_x86ub.log" | tr '\n' ' ' )" + fail=1 + fi else - echo " FAIL x86_64 UBSan slice RAN and exited $rc_ub without a sanitizer report: $( tail -2 "$WORK/out_x86ub.log" | tr '\n' ' ' )" + echo " FAIL 3b: the $X86_LABEL did not build with -fsanitize=undefined,integer: $( head -2 "$WORK/cc_x86ub.log" | tr '\n' ' ' )" fail=1 fi - # 3c CONTROL — a slice that runs and FAILS must read as FAIL, never as "no Rosetta": the x86_64 build - # of the mutation (arm 2's -DSTRKERN_MUTATE=1) is exactly that binary. - if X86MUT="$( compile_direct x86mut -arch x86_64 -march=x86-64-v3 -DSTRKERN_MUTATE=1 )" && [ -n "$X86MUT" ]; then + if X86MUT="$( compile_direct x86mut -arch x86_64 $X86_MARCH -DSTRKERN_MUTATE=1 )" && [ -n "$X86MUT" ]; then RIPWIRE_ROOT="$ROOT" "$X86MUT" > "$WORK/out_x86mut.log" 2>&1; rc_mut=$? if [ "$rc_mut" != 0 ] && [ "$rc_mut" != 132 ] && grep -qE 'FAILED|assertion|CHECK' "$WORK/out_x86mut.log"; then - echo " PASS 3c control: the mutated x86_64 slice RAN and failed on its own assertions (rc=$rc_mut) — a red, classified as a red" + echo " PASS 3c control: the mutated $X86_LABEL RAN and failed on its own assertions (rc=$rc_mut) — a red, classified as a red" elif [ "$rc_mut" = 132 ]; then - echo " FAIL 3c control: the mutated x86_64 slice died with SIGILL although the AVX2 probe ran — that is a real red, not the mutation" + echo " FAIL 3c control: the mutated $X86_LABEL died with SIGILL although the probe ran — a real red, not the mutation" fail=1 else - echo " FAIL 3c control: the mutated x86_64 slice exited 0 — the mutation is not visible on the AVX2 path" + echo " FAIL 3c control: the mutated $X86_LABEL exited $rc_mut without failing an assertion — the mutation is not visible on the $X86_PATH path" fail=1 fi + else + echo " FAIL 3c control: the mutated $X86_LABEL did not build: $( head -2 "$WORK/cc_x86mut.log" | tr '\n' ' ' )" + fail=1 fi else - printf ' SKIP no x86_64 UBSan cross slice on this toolchain; CI ubuntu-24.04 asan is the proof\n' + printf ' SKIP 3/3b/3c: no x86_64 cross slice on this toolchain (no macOS x86_64 SDK); CI ubuntu-24.04 runs the v3 slice natively\n' fi fi From 150fb6d37b045f01ad29284758e98aeb65f87a18 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 01:47:12 -0400 Subject: [PATCH 72/73] test(strkerncheck): the Rosetta probe exercises the whole x86-64-v3 feature set, not one AVX2 instruction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Run 7: the macos-14 runner's Rosetta 2 executed the one-instruction AVX2 probe and then SIGILL'd the v3 slice — so 'can run vpaddb' is not 'can run -march=x86-64-v3'. The probe is now compiled with the floor itself and touches every extension it implies (AVX2 broadcast/add, BMI2 pdep/pext and variable shifts, LZCNT/TZCNT, FMA, F16C, a MOVBE-eligible swap), every value through a volatile so nothing folds; any SIGILL in it routes the mirror to the baseline (scalar) slice. Verified locally: probe green → v3 slice (path=AVX2); probe forced off → baseline slice (path=scalar); 3c fails on its own assertions in both. --- test/strkerncheck.sh | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index cc8045c5f..7d81006bd 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -226,28 +226,46 @@ fi # `arch -x86_64 sysctl -n hw.optional.avx2_0` still prints 0 and leaf7_features lists no AVX2 — so the probe # EXECUTES one AVX2 instruction under Rosetta and reads the exit. rc 0 + "avx2 ok" => available; rc 132 # (SIGILL) or an exec failure => rosetta_no_avx2, and arms 3/3b/3c SKIP with that reason. Where the probe -# runs, a slice that exits nonzero — SIGILL included — is a FAIL, never a SKIP. +# runs, a slice that exits nonzero — SIGILL included — is a FAIL, never a SKIP. The probe is compiled with +# -march=x86-64-v3 itself and touches every ISA extension the floor implies (see the C below). ROSETTA_AVX2="unknown" rosetta_avx2_probe(){ cat > "$WORK/avx2probe.c" <<'EOF_PROBE' +// The probe must exercise the SAME feature set the slice is compiled with (-march=x86-64-v3 = AVX, AVX2, +// BMI1, BMI2, FMA, LZCNT, MOVBE, F16C), not one AVX2 instruction: PR #127 run 7 showed a Rosetta 2 that +// executes vpaddb and still SIGILLs the v3 slice. Every value flows through a volatile so nothing folds. #include +#include #include int main( void ) { - volatile int seed = 3; - __m256i a = _mm256_set1_epi8( (char) seed ); - __m256i b = _mm256_add_epi8( a, a ); + volatile uint64_t seed = 0x00F0F0F0F0F0F0F3ull; + volatile int sh = 3; + __m256i a = _mm256_set1_epi8( (char) seed ); // AVX2 broadcast + __m256i b = _mm256_add_epi8( a, a ); // AVX2 add unsigned char out[ 32 ]; _mm256_storeu_si256( (__m256i*) out, b ); - printf( "avx2 ok %d\n", out[ 0 ] ); - return out[ 0 ] == 6 ? 0 : 1; + uint64_t v = seed; + uint64_t r1 = _pdep_u64( v, 0xF0F0F0F0F0F0F0F0ull ) ^ _pext_u64( v, 0x0F0F0F0F0F0F0F0Full ); // BMI2 + uint64_t r2 = _lzcnt_u64( v ) + _tzcnt_u64( v ); // LZCNT / BMI1 + uint64_t r3 = ( v << sh ) | ( v >> sh ); // shlx/shrx (BMI2) + __m256 f = _mm256_fmadd_ps( _mm256_set1_ps( (float) sh ), _mm256_set1_ps( 2.0f ), _mm256_set1_ps( 1.0f ) ); // FMA + float fo[ 8 ]; + _mm256_storeu_ps( fo, f ); + __m128i h = _mm256_cvtps_ph( f, 0 ); // F16C + uint16_t ho[ 8 ]; + _mm_storeu_si128( (__m128i*) ho, h ); + uint64_t r4 = __builtin_bswap64( *(volatile uint64_t*) &v ); // MOVBE-eligible + printf( "v3 ok %d %llu %llu %llu %g %u %llu\n", out[ 0 ], (unsigned long long) r1, (unsigned long long) r2, + (unsigned long long) r3, (double) fo[ 0 ], (unsigned) ho[ 0 ], (unsigned long long) r4 ); + return out[ 0 ] == (unsigned char) ( 2 * (char) seed ) ? 0 : 1; // reaching this line at all is the fact; a SIGILL never does } EOF_PROBE - if ! "${CC:-cc}" -arch x86_64 -mavx2 -O1 "$WORK/avx2probe.c" -o "$WORK/avx2probe" 2>"$WORK/avx2probe.cc.log"; then + if ! "${CC:-cc}" -arch x86_64 -march=x86-64-v3 -O1 "$WORK/avx2probe.c" -o "$WORK/avx2probe" 2>"$WORK/avx2probe.cc.log"; then ROSETTA_AVX2="no_toolchain"; return 1 fi "$WORK/avx2probe" > "$WORK/avx2probe.out" 2>&1; local rc=$? - if [ "$rc" = 0 ] && grep -q '^avx2 ok' "$WORK/avx2probe.out"; then + if [ "$rc" = 0 ] && grep -q '^v3 ok' "$WORK/avx2probe.out"; then ROSETTA_AVX2="yes"; return 0 fi ROSETTA_AVX2="no (probe rc=$rc: $( tail -1 "$WORK/avx2probe.out" 2>/dev/null | tr -d '\n' ))"; return 1 From b4ebf0bbf0d82cb0e7e1b06df1dc6bb5be4f2b96 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Fri, 11 Sep 2026 02:15:13 -0400 Subject: [PATCH 73/73] test(strkerncheck): the probe's comment corrected, and the gate disassembles the probe to prove it carries the v3 opcodes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Correction of a fact the run-8 commit stated: run 7's probe did NOT execute an AVX2 instruction that Rosetta then survived — clang had folded the one-add probe to a scalar addb despite its volatile (otool: zero ymm/VEX opcodes), so it printed 'ok' on every runtime and never chose the baseline slice. Which x86-64-v3 extension Sonoma's Rosetta 2 lacks is not known; only that the v3 slice SIGILLs there. Hardening (coordinator's suggestion): after building the probe the gate disassembles it (otool -tv, or objdump -d) and requires one opcode of each class — ymm, pdep, pext, lzcnt, tzcnt, vfmadd, vcvtph2ps — so a future compiler that folds the probe makes it route to the baseline slice rather than lie; with no disassembler on the host the probe is treated as unverified. Run 8 (150fb6d3) proved the routing on the macos-14 legs: 30/31 green with the baseline slice there. --- test/strkerncheck.sh | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/strkerncheck.sh b/test/strkerncheck.sh index 7d81006bd..2628d90bd 100755 --- a/test/strkerncheck.sh +++ b/test/strkerncheck.sh @@ -232,8 +232,12 @@ ROSETTA_AVX2="unknown" rosetta_avx2_probe(){ cat > "$WORK/avx2probe.c" <<'EOF_PROBE' // The probe must exercise the SAME feature set the slice is compiled with (-march=x86-64-v3 = AVX, AVX2, -// BMI1, BMI2, FMA, LZCNT, MOVBE, F16C), not one AVX2 instruction: PR #127 run 7 showed a Rosetta 2 that -// executes vpaddb and still SIGILLs the v3 slice. Every value flows through a volatile so nothing folds. +// BMI1, BMI2, FMA, LZCNT, MOVBE, F16C). Its first cut was one AVX2 add compiled with -mavx2 — and clang +// folded that to a scalar `addb` despite the volatile (otool: zero ymm/VEX opcodes), so it printed "ok" +// on every runtime and never chose the baseline slice (PR #127 run 7). Which v3 extension Sonoma's +// Rosetta 2 lacks is not known; only that the v3 slice SIGILLs there. Hence: the floor's own -march, +// every extension touched, every value through a volatile, and the gate DISASSEMBLES the binary to +// assert the opcodes are really in it (below) — a probe that proves nothing must fail, not pass. #include #include #include @@ -264,6 +268,21 @@ EOF_PROBE if ! "${CC:-cc}" -arch x86_64 -march=x86-64-v3 -O1 "$WORK/avx2probe.c" -o "$WORK/avx2probe" 2>"$WORK/avx2probe.cc.log"; then ROSETTA_AVX2="no_toolchain"; return 1 fi + # The probe must CONTAIN the instructions it claims to execute — a folded probe (run 7) answered "ok" + # without a single vector opcode. Disassemble and require one opcode from each class; if no + # disassembler is on the host, the probe cannot be trusted and the mirror takes the baseline slice. + if command -v otool >/dev/null 2>&1; then + otool -tv "$WORK/avx2probe" > "$WORK/avx2probe.dis" 2>/dev/null + elif command -v objdump >/dev/null 2>&1; then + objdump -d "$WORK/avx2probe" > "$WORK/avx2probe.dis" 2>/dev/null + else + ROSETTA_AVX2="no (no disassembler to verify the probe's opcodes)"; return 1 + fi + for cls in 'ymm' 'pdep' 'pext' 'lzcnt' 'tzcnt' 'vfmadd' 'vcvtph2ps|vcvtps2ph'; do + if ! grep -qE "$cls" "$WORK/avx2probe.dis"; then + ROSETTA_AVX2="no (the probe binary lacks a $cls opcode — folded by the compiler; the probe proves nothing)"; return 1 + fi + done "$WORK/avx2probe" > "$WORK/avx2probe.out" 2>&1; local rc=$? if [ "$rc" = 0 ] && grep -q '^v3 ok' "$WORK/avx2probe.out"; then ROSETTA_AVX2="yes"; return 0

Paddle out with a map. See the rip before you’re in it. Trendshift: C++ Repository of the Day badge for redhat-et/ripwire