Skip to content

round(perf/strings/caps/quality/routing): the 2026-09-10 full-audit execution — one header of SIMD string kernels, the O(C²) child walks, the cache that evicted itself, per-kind --quality-delta dials, --help-task precision - #127

Merged
joyful-ii-V-I merged 87 commits into
mainfrom
integration/string-perf-2026-09-10
Sep 11, 2026

Conversation

@joyful-ii-V-I

@joyful-ii-V-I joyful-ii-V-I commented Sep 11, 2026

Copy link
Copy Markdown
Collaborator

Integration of the 2026-09-10 full-audit execution round: performance at scale, the string-handling class, cap correctness, --quality-delta per-kind dials, and --help-task precision. Twelve lanes, each built, gated red-first and byte-verified on its own branch, merged here --no-ff in dependency order so git log --first-parent names the lane behind any gate. Base: main edbb978d (one merge-main, after the eight PRs landed tonight). The audit that produced the findings ran read-only on main + #108 with llvm-project (182,555 files) as the scale rung; its plan and results stay local per house rule.

Owner decisions carried in this PR

  • x86-64 floor is -march=x86-64-v3 (AVX2, BMI1/2, FMA — RHEL 10's own floor). Prebuilt Linux x86 binaries now require AVX2-class CPUs. NEON on arm64. Every SIMD kernel ships a NEON path, an AVX2 path and a scalar twin, in ONE header.
  • Caps are blow-up guards toward one complete answer; nothing here lowers a value to flatter a byte count.
lane branch what proof
K strkern-tokenizer src/infra/strkern.h: nibble-table byte classification, A–Z fold, byteset/byte/3-byte find (NEON/AVX2/scalar); the query-time tokenizer rewritten as mask algebra, proven against verbatim copies of the old walkers; BM25 head-mask index; readWholeFile on the scan path --pack-task go −27.7% CPU, rocksdb −10.3%, ripwire −13.3%; 18/18 byte-identical; AVX2 mirror green under Rosetta 2
K2 strkern-onefile-doctest Byteset256 carries its own derived tables (the tail no longer rebuilds the set per call); one header; kernel tests as a doctest TU (test/verify_strkern.cpp, target ripwire_test_strkern) 19 TEST_CASEs incl. mutation controls; 21/21 byte-identical
M emit-grep-kernels XML/JSON escapers copy clean runs between the bytes a 256-bit set finds; the --grep SIMD scan was REFUTED (the verb is file-open bound; scan is 0.4% of busy) escaper share 4.6→2.0% / 6.3→2.6%; 24/24 identical
F fieldid-regex 199 ts_node_child_by_field_name sites → a per-grammar TSFieldId table (F3 one layer down); std::regex compiled once per predicate cold CPU go −8.8%, rocksdb −5.4%; --lint −12 to −15%; 1,189,205 enumerated (node, field) pairs; ASan+TSan clean
W preprocdead-cursor collectPreprocDeadRanges's indexed child walk was O(C²) (the vendored iterator restarts per call) llvm cold 202 → 170 s CPU (−15.7%), wall −28.6%; gate red at 62× before
W2, W3 childwalk-sweep, childwalk-final the other 23 quadratic child walks (10 input-bounded + the "grammar-bounded" ones the fixture refuted, incl. bindsVisitNode, captureBases, callArity, the --slice flow walker) on src/infra/tschildren.h 18 isolation arms red-first at 11–125×; 50+ byte-identical pairs
C, C2 cache-pin-reserve, cache-rootkey-unify eviction pins every family of the working root (the 2 GB sweep was evicting llvm's own rich blob: same --for 274 → 26 s CPU); one root key for all seven cache families (two FNV seeds, one truncated) disclosed on stderr only when something was evicted
Q qd-perkind-dials --quality-delta per kind: churn="self" informational; dead-code sees headers (excludes language-invoked symbols instead); verbosity counts code lines; cx/verbosity gate on a crossing or ≥25% growth; api-surface new symbols → a count; overload-set/vendored clone drops; comment-only swallow 12 landed commits: 12/12 → 8/12 gating, TRUE 2% → 12%, WRONG → 0; synthetics caught 5 → 7
R helptask-precision --help-task: "how does …" no longer mints a symbol; JSON keys never resolve; stop rules gated as present and load-bearing; router names 16/16 skills; MCP no_route harmful 13/25 → 0, precision 0.797 → 1.000; hook↔CLI 10/10
H capregister-harness capsweep harness honesty (6 defects); cap register 120 → 212 declarations + BOUNDARY class; kHandoffSymbolsPerFile 6 → 50 code / 12 prose (containment 16 → 54%, additive); disclosure totals; tests_capped derived re-derived split 64 of 151 answering rows (was published as 59/195)
H2 listing-paging --doc-drift, --flags/--flip, --situ disclose their cuts and page; answer rows never page 33-check gate, 24 red before

Gate pins this PR moves (for review, not just the commit messages):

  • donelegendcheck qd_dirty ceiling 3800 → 3900 B (a loosening). The --quality-delta legend gains two facts a reader needs to act on a row: the api-new-surface= count (one sentence; its "printed even at zero" phrase is emittertruthcheck's roster key, probed by the new Z2g arm) and the churn facets' gating rule (one clause). Measured on the gate's own fixture, pre-round binary → this tip: clean 2177 → 2282 (ceiling 2300, unchanged), dirty 3642 → 3855 (ceiling 3800 → 3900), scope 4916 → 5129 (5200, unchanged), refpair 4058 → 4271 (4300, unchanged). Lane Q's first cut of those sentences was 476 B larger; they were trimmed to one clause each so three of the four forms stay under their old ceilings, and the dirty form's headroom shrinks from 158 B to 45 B — the ceiling moves by less than the growth.
  • w3fixlegendcheck --situ section [2]: the 25-row cap on tests-to-run was RETIRED (answer rows never page; listingpagingcheck arm D pins the retirement red-first), so the arm now asserts every row listed AND no cut disclosed — listed rows equal the header count, and no cap attribute or wording remains in the section, the legend, or src/ (kSituTestRowsShown is gone).
  • cppqualcheck live-repo pin --uses=readWholeFile 22 → 23 (src/lexical.h adopted the documented reader on the BM25 scan path; history line added in the gate).
  • mcpmanifestcheck ceiling 41,650/42,000 → 42,200 (attributed tool by tool in the gate header, above).
  • fieldidcheck links its harness against the build's grammar objects; a Release build leaves them as LTO bitcode, so the gate now retries the identical link with -flto (macOS's linker read them transparently, which is why only the ubuntu Release legs were red).

Regenerated at the tip, never merged by hand: gate count (597, generator), docs/LIMITS.md (with #123's by-name pins and H's widened census), docs/TUNING.md (from the clean sweep), docs/COMMANDS.md, test/printf_parity.manifest, the ack ledger healed through the binary (1,249 → 1,233 rows), the MCP manifest ceiling re-anchored at 42,200 with both lanes' bytes attributed, docs/LINEAGE.md rows for the sources whose technique is in the shipped code (49 repositories / 70 papers).

Upgrade note for the release: the cache root key is now one derivation for every family (the lean/rich builder had hashed the root with a truncated FNV basis, the git-metadata families with the real one), so after upgrading, existing lean/rich/qchurn/MCP cache blobs written by older builds are clean cache misses — one cold parse per root — and are swept by the age pass. No scheme constant moved: blob contents are unchanged, only their names.

Not in this PR, recorded: ~25 further indexed ts_node_child loops in six files W3 listed for a follow-up (the parent-chain family now owns the #1 leaf); the second half of C2 F16 ("would have entered the answer" for the doc-mention flag); the deck .pptx is regenerated from its generator at release time.

Before/after, main's binary (9356cf23 source) vs this tip — same argv, interleaved arms, CPU = user+sys, median over n pairs (min in the Δmin column), on a shared box (load 7–14). Every row cmp-identical between the two binaries.

corpus invocation main CPU s this PR CPU s Δ median Δ min output n
ripwire (own tree) cold map --no-cache 4.28 4.19 -2.1% -1.0% identical n=5
ripwire (own tree) warm map 0.13 0.14 +2.0% +2.3% identical n=5
ripwire (own tree) warm --for (conceptual) 0.30 0.31 +3.5% +0.2% identical n=5
ripwire (own tree) warm --pack-task 0.45 0.38 -15.5% -15.6% identical n=5
ripwire (own tree) warm --grep 0.43 0.44 +1.7% +1.2% identical n=5
ripwire (own tree) warm --lint 4.21 3.91 -7.3% -6.8% identical n=5
go cold map --no-cache 16.29 16.01 -1.7% -5.1% identical n=5
go warm map 0.66 0.66 -0.6% -1.3% identical n=5
go warm --for (conceptual) 1.89 1.96 +3.9% -0.0% identical n=5
go warm --pack-task 8.13 5.88 -27.6% -28.0% identical n=5
go warm --grep 2.18 2.14 -2.0% -2.0% identical n=5
go warm --lint 18.43 15.18 -17.7% -16.7% identical n=5
rocksdb cold map --no-cache 7.96 7.42 -6.7% -4.6% identical n=5
rocksdb warm map 0.31 0.31 -2.4% -1.0% identical n=5
rocksdb warm --for (conceptual) 0.62 0.63 +1.6% +0.1% identical n=5
rocksdb warm --pack-task 0.68 0.60 -10.9% -10.8% identical n=5
rocksdb warm --grep 0.61 0.62 +0.7% +1.2% identical n=5
rocksdb warm --lint 6.77 5.60 -17.3% -18.7% identical n=5
llvm-project cold map --no-cache 194.14 155.60 -19.9% -19.9% identical n=1
llvm-project warm map 10.49 10.41 -0.8% -0.8% identical n=1
llvm-project warm --for (conceptual) 19.59 19.15 -2.3% -2.3% identical n=1
llvm-project warm --pack-task 28.45 27.02 -5.0% -5.0% identical n=1
llvm-project warm --grep 26.96 25.64 -4.9% -4.9% identical n=1

Reading the table: the cold parse (--no-cache) carries the child-walk and field-id lanes (−2% on this tree, −5% go min, −7% rocksdb, −20% llvm — the O(C²) walks only bite on wide flat child lists, which C/C++ include guards and comment floods produce); --pack-task carries the tokenizer + head-mask lane (−28% go, −16% ripwire, −11% rocksdb, −5% llvm); --lint carries the regex hoist (−7% / −18% / −17%); warm map, --for and --grep are within noise (their floors are the serial resolve loop and file opens, both named in the audit as the next rung). Not in the table: the cache-eviction fix (a same-argv --for on llvm went 274 → 26 s CPU when the sibling family had been evicted; it now never is), and the verbs whose OUTPUT this PR changes by design (--quality-delta, --help-task, --handoff, --doc-drift, --flags, --situ).

🤖 Generated with Claude Code

joyful-ii-V-I and others added 30 commits September 10, 2026 18:00
… mirrored paths, gated

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.
…dow rewrites

`--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 65d98b7'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 (bc517e0 03ec6f1 dfcb57b e5f9bff 65d98b7
4ee920e d7873f8 b392b29 e5b2ad8 9e9fce1 4c24b8d 7d5dd20), 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 <noreply@anthropic.com>
…ures

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
+ &#9;/&#13; + 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).
…nd its own output was in the corpus

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 <f> 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.
`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 0841640 — 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 <noreply@anthropic.com>
…equires

`does` was a symbol-slot cue AND `how does` is the understand-symbol gate, so every English
question of the form "how does <indexed-word> …?" minted the very symbol the gate then demanded —
the same two words playing both parts. 13 of 25 adversarial prose prompts recommended
`--expand=<English word>`, 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 <lowercase-name> 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 <noreply@anthropic.com>
…ape of one habit, not of a population

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.
…e exactly, load-bearing differentially

#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 <noreply@anthropic.com>
…4 -> 170.46 s CPU, map byte-identical

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.
…AST node

`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<A.min | load |
| --------------------- | -- | -------- | -------- | ------------ | --------- | ----------- | ---- |
| go (15,865 files)     | 6  | 18.214 s | 16.609 s |   -8.81%     |  -7.76%   | yes         | 16.8 |
| go (15,865 files)     | 12 | 19.177 s | 17.858 s |   -6.88%     |  +1.21%   | no          | 32.7 |
| rocksdb               | 12 |  8.204 s |  7.762 s |   -5.40%     |  -6.06%   | yes         | 34.9 |
| ripwire tree          | 12 |  4.541 s |  4.486 s |   -1.21%     |  -1.47%   | no          | 31.7 |

The two go rows are the same arms at two machine loads; the 12-run pair ran with five other
lanes on the box (load 33) and its min is noise, which is why the quiet 6-run pair is printed
beside it rather than instead of it. The ripwire tree's smaller win is honest and the sampler
says why: that corpus's own strncmp share is 2.56% of busy, not go's 3.44% — it is
markdown-heavy, so less of it is AST walk.

BYTE-IDENTICAL, 22 of 22 arms, three corpora (go / rocksdb / a pristine ripwire tree), seven
verbs: default map --top-k=100000, --lint, --grep, --for, --pack-task, --match, --clones,
--hotspots, --slice, --uses. go --top-k=100000 = 10,415,057 B, matching the figure the audit
round recorded. Determinism: two runs byte-identical; `xmllint --noout` clean.

GATE FIRST — test/fieldidcheck.sh, seven arms, all with can-go-red controls:
  A0  the UNWARMED lookup equals ts_language_field_id_for_name (943 pairs) — "a missed warm is
      slower, never wrong" is tested, not asserted in a comment.
  A   the warm table equals it too, over 23 grammars x 41 fields harvested FROM THE TREE
      (kLangTable and the header, at gate time — not a list frozen in the gate).
  B   enumerated pairs: 1,189,205 ( node, field ) comparisons over 29,005 nodes of 32 fixture
      files, fieldChild vs ts_node_child_by_field_name, node-for-node.
  C   unknown-field parity, and it is NOT vacuous: 583 of the 943 grammar-field pairs resolve
      to id 0, and ts_node_child_by_field_id( n, 0 ) is the null node by node.c:602.
  D1/D2 mutation: misspelling one row of kNodeFieldNames (the data) and shifting the field index
      inside the lookup (the code) each turn arms A/B red — 677 and 21,465 disagreements.
  E/E-warm  population: zero ts_node_child_by_field_name left, 199 fieldChild sites, the warm
      wired over kLangTable and called from ingest(); both with revert controls that prove the
      arms are not green in both directions. Arm E reads SOURCE, and says so.
  F   capacity — now also a static_assert in ingest_crawl.h, so a 65th language is a compile
      error rather than a run that silently keeps the by-name path.

Also here, because --quality-delta found it as a duplication the moment the two call sites
normalised alike: the `spelling.empty() ?` guard in preprocIncludeTarget/preprocImportTarget was
dead — includePathOf( "" ) already returns "" through its own size < 2 arm and leaves isAngleOut
alone. Removing it clears the one new gating clone pair the conversion introduced.

--quality-delta after: 97 gating rows, 96 of them short-horizon-churn "self" on the nine files a
199-site mechanical conversion necessarily touches (the change's own footprint, unavoidable and
not acked), and one api-surface contract-change — nodeFieldText losing its fieldLen parameter,
4 params to 3, which is the point. --test-gate: exit 4 as any change gives; the 21 named tests
plus the slice/preproc family were run and are green.
…at changed — 6 becomes 50 code / 12 prose

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 <f> rows removed, 0 <s>
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=<cap-6 build>):
  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.
… read, all off the per-byte path

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. <fstream> and <sstream> 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.
…he code

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 05f4b89'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 <noreply@anthropic.com>
…ys what it took

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 (05f4b89)   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 <noreply@anthropic.com>
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 7d5dd20 ("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 (03ec6f1) 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 <noreply@anthropic.com>
…bout 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 33ea149; 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.
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 <noreply@anthropic.com>
…ld never say yes

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 <t> 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 <t> 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 <doc> 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.
… the whole skill catalog

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 <noreply@anthropic.com>
… per match

`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<A.min | load |
| ------------ | ------------------------------------- | -------- | -------- | --------- | --------- | ----------- | ---- |
| go           | `--lint` (warm)                       | 18.055 s | 15.809 s |  -12.44%  |  -8.67%   | yes         | 15.4 |
| rocksdb      | `--lint` (warm)                       |  6.817 s |  5.788 s |  -15.10%  | -16.91%   | yes         | 21.0 |
| ripwire tree | `--lint` (warm)                       |  4.398 s |  4.084 s |   -7.14%  |  -3.32%   | no          | 20.9 |
| go (n=10)    | `--match=… (#match? @c "^(make\|append\|copy\|len)$")` | 13.835 s | 12.703 s | -8.18% | -7.66% | yes | 19.6 |

NON-DEGRADATION on a path with no predicates — the table is still built when the query compiles:
go cold `--no-cache --top-k=100000`, n=10, 18.241 s -> 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.
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 (5723b2c, 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 <noreply@anthropic.com>
…riable

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.
… and the SIMD scan refused

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.
… not your duplication

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 (0841640, 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 <noreply@anthropic.com>
…e gate that catches all ten

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 e246ca2), 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 e246ca2: 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 <noreply@anthropic.com>
…ildren — --pattern 3.76s -> 0.06s

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 e246ca2: --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 <noreply@anthropic.com>
…ss-2 sites worth converting

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 e246ca2
(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 e246ca2: 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 <noreply@anthropic.com>
…whole root, not half of it

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-<old-key>-….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 (5723b2c): 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 <c n=…> 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`--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 `<sa>` 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 <noreply@anthropic.com>
… 2 without AVX2, not a red

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).
…TED an AVX2 instruction

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).
…e where Rosetta 2 has no AVX2

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.
…eature set, not one AVX2 instruction

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.
…sembles the probe to prove it carries the v3 opcodes

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 (150fb6d) proved the routing on the
macos-14 legs: 30/31 green with the baseline slice there.
@joyful-ii-V-I
joyful-ii-V-I merged commit b01df96 into main Sep 11, 2026
31 checks passed
joyful-ii-V-I added a commit that referenced this pull request Sep 11, 2026
…s/LIMITS.md regenerated over the merged tree, not hand-merged

Only docs/LIMITS.md conflicted. docs/limits_build.py and test/limitstablecheck.sh auto-merged. `## Caps, by file`
still sits right before render()'s per-file loop, beside #127's widened census and BOUNDARY class, and arm (K)
sits beside #127's (H).

Resolution: `python3 docs/limits_build.py` over the merged tree gives 208 caps, 7 parameters and 111 classified.
--check rc 0, and all 83 per-file tables sit under `## Caps, by file`. Against main, the regenerated file differs
only by that heading and its sentence.

limitstablecheck 23/23 PASS, including (H) and (K). gatecountcheck 0, manifestcheck 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 11, 2026
…#127 landed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 11, 2026
…o file in common, nothing to regenerate

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
andriytyurnikov pushed a commit to andriytyurnikov/ripwire that referenced this pull request Sep 11, 2026
…edhat-et#127 head this lane stacks on (test/fieldidcheck.sh only)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
andriytyurnikov pushed a commit to andriytyurnikov/ripwire that referenced this pull request Sep 11, 2026
…dhat-et#131 third-party row) joins the std::-guard fix

Textual conflicts, all at gate-count and gate-list sites:
  README.md, docs/EVALS.md, present/deck5_ripwire_build.js   main's text taken, then the count sites
                                                               regenerated by docs/gatecount_build.py on
                                                               the merged loop: 597 + stdqualcheck = 598
  test/regression.sh                                          union of member sets — every main entry kept
                                                               (redhat-et#127's nine appended gates included),
                                                               stdqualcheck in its alphabetical place

docs/COMMANDS.md and docs/captures/COMMANDS_showcase_2026-09-11.md merged clean as TEXT but describe
neither parent's binary: the capture was recorded on the pre-redhat-et#127 tree, and redhat-et#127 changed --help (--limit
now honored by --flags/--situ). They are regenerated on the merged, rebuilt tree in the next commit,
never hand-merged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
andriytyurnikov pushed a commit to andriytyurnikov/ripwire that referenced this pull request Sep 11, 2026
… tree — the 09-11 capture predated redhat-et#127

The 09-11 capture merged clean as text but was recorded on the pre-redhat-et#127 tree (built_from=d39554ddb), so it
described neither parent: redhat-et#127 changed --help (--limit now honored by --flags/--situ, the answer-rows-never-
page sentence) and a long list of verb outputs. Regenerated, never hand-merged:

  docs/captures/COMMANDS_showcase_2026-09-11.md   test/showcase_capture.py on the CLEAN merge commit 4f9c715,
                                                  binary stamped built_from=4f9c71537 after a --clean-first
                                                  rebuild; 253 commands, the non-zero exits the captions
                                                  promise (refusal demos) and no others; PYTHONDONTWRITEBYTECODE=1
                                                  with docs/__pycache__ removed first, so the crawl it records
                                                  carries no bytecode dir. bodySeed re-derived the seed as
                                                  src/graph.h:3194 and it publishes sym="rankGraphTeleport"
                                                  (main did not touch graph.h, so the seed did not move)
  docs/COMMANDS.md                                docs/docs_commands_build.py --bin build/ripwire from that
                                                  capture (175 flags, 161 samples; --check clean)

Same file name as before: the capture is dated by the day it runs, and the newest capture is the one the
gates and the generator read. main carries no newer capture (its newest is 09-10, kept as history).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to xCatG/ripwire that referenced this pull request Sep 11, 2026
…t#130 child walks, redhat-et#132 out-param readers, redhat-et#127, redhat-et#128, redhat-et#129, redhat-et#131, redhat-et#133) joins the tier-3 disclosure fix

Three textual conflicts, each resolved from both sides:
  test/regression.sh   the gate loop is the union: main's line (stdqualcheck in its sorted place) plus this
                       branch's declinecheck at the tail; docs/gatecount_build.py re-stamped README, EVALS and
                       the deck at 599 (--check clean)
  docs/COMMANDS.md,    main's side taken verbatim (add/add on the capture); both are regenerated on the rebuilt
  docs/captures/COMMANDS_showcase_2026-09-11.md   merged tree in a later commit, not hand-merged

Auto-merged without conflict: src/graph.h, src/mcp.h, src/mcpverbs.h. main's side of mcp.h/mcpverbs.h is the
redhat-et#132 out-param returns in the quality verbs; this branch's side is the declined_calls description and keys.
They touch different hunks and share no function.

NOT FINISHED BY THIS COMMIT, on purpose so the next commit carries the evidence: redhat-et#134's std:: guard exit in
buildGraph's resolve loop calls vetoExternal( r ) and discards the CallDisposition this branch made it return,
so every site the guard refuses leaves the loop Unaccounted and a plain build raises DEGRADED_PATH_ALERT. The
next commit names it, with a declinecheck arm shown red on this tree first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to xCatG/ripwire that referenced this pull request Sep 11, 2026
…ted External, as the external= it raises

redhat-et#134's std:: guard (keepStdQualifiedCandidates) entered buildGraph's resolve loop in the merge above as
`vetoExternal( r ); continue;`. On this branch vetoExternal returns the CallDisposition it counts, and the guard
threw it away, so every site the guard refused left the loop Unaccounted. The conservation line did not balance,
the census's external fell below the header's by exactly those sites, and every plain build raised
DEGRADED_PATH_ALERT. The exit now reads `disposition = vetoExternal( r );`.

External, not QualifiedExternal: the guard raises external= and writes one `C external` census row, and arm F pins
census external == header external. qualified_external stays the Rust guard's bucket, which raises no gauge.
pincensus.h's External comment now names every vetoExternal caller, the std:: guard among them.

LOOP-EXIT AUDIT over origin/main 5c80848..766913d: the std:: guard is the only exit main added to the resolve loop.
redhat-et#127, redhat-et#130 and redhat-et#132 changed no line inside it; graph.h's only other main hunk is resolveAtSeed's readWholeFile (redhat-et#132).
Every exit after the tally names a disposition. The `continue`s in the multi-root admission loop and in the edge
accumulation loop, and the two one-line `break`s, end INNER loops; the loop has no break, return or goto at its own
level.

ARM (test/declinecheck.sh F): a third conservation run, over redhat-et#134's own fixture test/stdqualfix, whose std::move and
std::swap sites have lone in-repo decoys and so reach the guard with candidates. Premise: takeTwice's two std::move
sites are external census rows, which only the guard can write for a qualified call (the Phase-5 veto requires an
empty qualifier). Then the line conserves, census external == header external, and stderr carries no alert.
test/declinefix and its pins are untouched.

RED, merge commit 3cc236e (plain build --clean-first, built_from=3cc236e9e), without this line:
  declinecheck  3 FAIL / 101 PASS: exactly the three new rows; their premise passes
                stdqualfix  calls=18 bound=6 external=2 unaccounted=10, header external=12, alert at graph.h:2812
  ripwire .     calls=135449 external=1005 unaccounted=1495 (header external=2500), alert
  memgraph      calls=295086 external=7404 unaccounted=4966 (header external=12370), alert
GREEN, this commit (plain build --clean-first):
  declinecheck  104 PASS / 0 FAIL; stdqualcheck PASS
  ripwire .     # dispositions calls=135449 bound=27704 self=42 external=2500 unresolved=4145 undefined=21105
                other_root=0 qualified_external=6 declined=6263 file_scope=73684 unaccounted=0
  memgraph      # dispositions calls=295086 bound=150305 self=1122 external=12370 unresolved=9811 undefined=41388
                other_root=0 qualified_external=105 declined=65516 file_scope=14469 unaccounted=0
  no DEGRADED_PATH_ALERT on either. memgraph's map is byte-identical red vs green: the line counts, it moves no edge.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 11, 2026
Both notes beside the Round 4 tables now say how big the tree behind the 194.1 s to 155.6 s cold-parse
figure is (PR #127: "llvm-project (182,555 files) as the scale rung").

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit that referenced this pull request Sep 11, 2026
The sentence "Every install route ... is in INSTALL.md" sat right under the quick-install block on main before
PR #127 (b01df96^1, README line 81). The #127 head never had it, the merge took that side, and main has not
linked INSTALL.md from the README since. It goes back in the same place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to xCatG/ripwire that referenced this pull request Sep 11, 2026
…rse lane

Main moved from 5afadec through redhat-et#127 (b01df96), redhat-et#131, redhat-et#133, redhat-et#132, redhat-et#128, redhat-et#129 and redhat-et#130.
Git merged every file but one as text; the one conflict is a pin, re-derived rather than picked.

- test/qschemetrip.hash: CONFLICT, both sides re-pinned for their own half of the manifest (redhat-et#132
  moved readRegisterMacrosConfig's read; this lane moved kCacheVersion 18 -> 20 and kParserVer
  88 -> 90). Re-pinned on the merged tree with UPDATE_GOLDEN=1 -> d8dcd5c0; the RE-PIN LOG in
  test/qschemetripcheck.sh now records the lane's bumps and why neither side's hash was carried.
- Auto-merged, read hunk by hunk: src/quality.h (redhat-et#132's optional<string> reads vs this lane's
  mirror block), src/ingest_names.h (redhat-et#130's cursor walks vs parseRecoveredBits),
  src/ingest_sidecap.h (redhat-et#130's pybindDefParts vs the re-parse wiring), README.md (redhat-et#133's hero
  vs the gate-count sites). Disjoint regions; nothing reinterpreted.
- Version constants stay this lane's: kParserVer 90, kCacheVersion 20, and quality.h's
  kIngestParserVerMirror / kIngestCacheVersionMirror 90 / 20 (main is still 88 / 18).
- Generated sites checked on the merged tree: docs/gatecount_build.py --check (599 = main's 597
  plus extentcheck and macroreparsecheck), docs/limits_build.py --check (208 caps).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to xCatG/ripwire that referenced this pull request Sep 11, 2026
…64-v3 binary it builds; its Xcode and minimum macOS are pinned

88a8f65 makes the macos-x64 binary x86-64-v3, and this leg EXECUTES it on its arm64 runner under
Rosetta 2: pgobuild's nine training runs, the determinism diff, --version and the smoke test. The binary
contains AVX2 (44 vpmovmskb on ymm), FMA, BMI1/2, LZCNT (314) and MOVBE (108). macos-14's Rosetta SIGILLs
a v3 slice (test/strkerncheck.sh, PR redhat-et#127 run 4), so on that runner the leg fails at training. macOS 15's
Rosetta gained AVX2, but its LZCNT/MOVBE coverage is unverified. macOS 26's is verified: on a macOS 26.5.1
host with Xcode 26.6 (17F113, the macos-26 image default) this leg's exact pgobuild (Release,
CMAKE_OSX_ARCHITECTURES=x86_64, MACOSX_DEPLOYMENT_TARGET=14.0) collected a profile under Rosetta, and
build_pgo/ripwire passed every post-step (emit=std::print, minos 14.0, determinism diff, xmllint) with
output byte-identical to an arm64 build on test/fixture, the repo map and a --for query.

- macos-x64: os macos-14 -> macos-26; developer_dir Xcode 26.6.
- deployment_target "14.0": exported by a step (a leg without the key never sees the variable) and read
  back off the binary (otool minos). Without it clang takes the lower of the runner's macOS and the SDK
  default, 14.x on macos-14 but 26.0 on macos-26, so the move alone would drop macOS 14/15 Intel users.
- DEVELOPER_DIR is per leg (matrix.developer_dir); macos-arm64 keeps Xcode 16.2 on macos-14, unchanged.

Gate first: test/portablebuildcheck.sh #2h (text-level, every host). Red on 88a8f65 (the leg on
macos-14; no pin); ALL PASS here. actionlint clean; test/releaseinstallcheck.sh ALL PASS. quality-delta:
two minor, non-gating verbosity growths on the already-over-bar jobs/build YAML keys (rationale comments).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to andriytyurnikov/ripwire that referenced this pull request Sep 12, 2026
…ty half leads

The showcase cover now matches docs/assets/banner.svg:
- a terminal card with the three dots;
- the two-tone name, rip in cyan and wire in amber;
- "The ripgrep of AI context.";
- the wave from the README hero directly under that line;
- the one-sentence description in the right column.

Below the card the two halves are weighted on purpose. rip, the speed half, is the narrower card. wire,
"the other half: honesty", is the wide amber-framed card: floors, "a zero means none found", every
truncation disclosed, and every guess saying how many it chose from.

The wave moves out of the README (its hero now carries only the Trendshift badge under the intro).
docs/assets/paddle-out.svg stays in the repo. The deck embeds present/assets/paddle-out.png, rendered
from it at 4x on the card colour with Arial, because pptxgenjs writes an SVG's bytes into the PNG
fallback slot, which older viewers cannot draw.

The grammar count said 21 in four places. It is 23: 22 upstream projects under third_party/deps/,
with tree-sitter-typescript supplying both typescript and tsx. The deck's language row gains Elixir
and Dart, in the README's order.

Slide count is unchanged; deckcheck and deckclaimcheck pass. The pptx/pdf regenerate when this lands
after redhat-et#127.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to andriytyurnikov/ripwire that referenced this pull request Sep 12, 2026
…three honesty stories, a --quality-delta examples slot

The deck grows from 29 slides to 33. Every figure on the new slides is quoted, with its origin, in that slide's
speaker notes. Everything here is merged on main 766913d; claims about redhat-et#135, redhat-et#136 and redhat-et#126 are in the next commit.

- 0.6.0 at a glance: Dart, the 23rd grammar (6f91fed), and the six hand-sized language arrays its landing caught;
  Rip'n Fast (redhat-et#83 warm --grep 159.7 -> 9.2 s; redhat-et#127 llvm cold parse -19.9% CPU, go --pack-task -27.6%); honest where
  it counts (redhat-et#134 2,107 -> 3; redhat-et#127 per-kind --quality-delta 12/12 -> 8/12, --help-task 13/25 -> 0); fewer tokens,
  nothing hidden (redhat-et#92 --help 46,385 -> 4,473 tokens; redhat-et#127 --handoff 6 -> 50, listing verbs page). Footer: the
  x86-64-v3 upgrade note from redhat-et#127's body.
- The scale rung: the O(C^2) child walks (redhat-et#127, redhat-et#130), the inheritance cone (redhat-et#83) and the cache that evicted itself
  (redhat-et#127), each with its llvm-project number; a per-corpus bar row from redhat-et#127's table. The title says "found at
  llvm-project scale", because only the child walk is sourced as invisible on the standard corpora. One callout
  lists what is built on or with LLVM. Each item was checked against a primary page, and the URL and quote are in
  the notes. XLA was dropped: no fetched openxla.org page says it is MLIR-based. The PS4 is the only console named,
  because only Sony's 2013 PS4 toolchain slides were read.
- Honesty stories: std::move's 2,107 callers (redhat-et#134); the probe that lied (b4ebf0b corrects 150fb6d's reason:
  clang folded the one-add probe to a scalar addb, and the gate now disassembles its own probe); the audit's
  instrument was wrong twice (the redhat-et#127 brief, 209f97a, 5723b2c, redhat-et#127 lane H's 59/195 -> 64/151).
- What --quality-delta catches: a marked PLACEHOLDER. qdExamples() renders QD_EXAMPLES, a data-only array at the
  top of the generator, as 1-3 wide or 4-6 two-column before/after cards in the mono face. The array is empty
  until docs/QUALITY_DELTA_CATALOG.md lands, and the helper refuses more than 6 entries or an empty field.

The two drawing helpers (storyCards, qdExamples) take a slide their caller added. That keeps one literal addSlide
call per slide, which is what test/deckclaimcheck.sh counts. A first cut added the slide inside the helper: the
count still read 33, but only by coincidence, and a second call to the helper would have drifted from the pages
while the gate stayed green.

The cover and its wave are 680a0a3's, unchanged apart from notes that source its 23-grammar count. The slide count
moves in README.md (two places), present/README.md and the re-derive row. The pptx and PDF are regenerated
(pptxgenjs 4.0.1; LibreOffice 26.2 impress_pdf_Export; 33 pages).

Gates on this tree with a main-tip binary (built_from=766913d02): deckcheck ALL PASS (0 bad values, 0 stale),
deckclaimcheck ALL PASS (179 long flags, 33 slides), readmedriftcheck ALL PASS.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
joyful-ii-V-I added a commit to andriytyurnikov/ripwire that referenced this pull request Sep 12, 2026
…strument, the scale rung, count every omission

prompts/full-audit.md was last changed on 2026-08-08. This folds in what the 2026-09-10 round (redhat-et#127
and follow-ups redhat-et#128..redhat-et#136) taught:

- A "measure the instrument" section before any lens: cache eviction that faked super-linearity, a
  cap sweep on a population of zero, and a capability probe folded to scalar.
- Lens 1: hostile inputs (vendored-scanner aborts, redhat-et#126's review) and conservation lines (redhat-et#136's
  dispositions).
- Lens 2: the scale rung and red-first isolation arms.
- Lens 3: the adversarial prose set.
- Lens 6, new: the honesty of the output.
- Process: regenerate generated artifacts on the merged tree; a targeted gate list is not the suite
  (redhat-et#132); cross-ISA slices carry sanitizer flags.
- How to run the audit with ripwire on your own large repository, and how to file findings.

prompts/README.md: the full-audit row describes the new lenses.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

(cherry picked from commit 9d931b0b6a69ae0f64989e3b679c0d9f7e568625)
neoneye pushed a commit to agent-memory-atlas-archive/redhat-et--ripwire that referenced this pull request Sep 13, 2026
…dule/name/arity resolution) — parser version 95

REHEARSAL ONLY — local branch pr81-rehearsal, never pushed. Resolves the 15 conflicted files of
merging main into @henry-hz's head 9046edc, assuming PR redhat-et#139 (kParserVer 93, kCacheVersion 21) and
PR redhat-et#172 (kParserVer 94) land first:

- src/ingest_cache.h, src/quality.h: kParserVer / kIngestParserVerMirror 95 (branch carried 87, main 92);
  kCacheVersion / kIngestCacheVersionMirror 21 (redhat-et#139's format; redhat-et#81 changes no record shape).
- src/ingest_elixir.h, src/ingest_sidecap.h: the branch's semantics kept (ElixirContext scopeOf/moduleOf,
  attribute refs, alias/atom/dot/__MODULE__ receivers, the refined @-attribute filter); converted to
  main's redhat-et#127 API (fieldChild/NodeField, kindIs) since the by-name nodeFieldText overload is gone on
  main. NodeField gains Operand (src/infra/fieldid.h). #include <charconv> added (review item 1).
- src/graph.h: PROVISIONAL accounting on the Elixir no-candidate drop — disposition Undefined/Unresolved
  instead of Unaccounted, so main's 6b8dc9d conservation alert stops firing on every Elixir corpus
  (it fired on test/elixirfix: 10 of 19 sites). No edge changes; review item 2's design choice stays open.
- test/regression.sh: gate-list union (606 + elixirsemanticcheck = 607); test/gateexitcheck.sh: both rows.
- test/qschemetrip.hash, test/printf_parity.manifest, gate-count sites, docs/COMMANDS.md: regenerated
  from the merged build. docs/captures/*: main's side (the 09-12 capture's graph.h seed now needs a
  ref-clean re-record: rankGraphTeleport moved 3357 -> 3371).
- CHANGELOG.md, README.md, docs/ARCHITECTURE.md: both sides, renumbered to 95/21/10.

Wanted gate set after this commit: gates=12 pass=11 fail=1 — the one failure is showcasecapturecheck
arm (H), the seed line, which only a re-recorded capture can fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants