From 1c7dbac0041669ae52f7e3f3307cddab6cc010b5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 12:25:35 -0400 Subject: [PATCH 1/8] =?UTF-8?q?perf(graph):=20the=20super-linear=20warm=20?= =?UTF-8?q?floor=20was=20the=20CHA-lite=20cone,=20recomputed=20per=20call?= =?UTF-8?q?=20=E2=80=94=20memoised,=20byte-identical?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tgrep head-to-head left warm --grep at 40 us/file up to 15,865 files and 938 us/file at 182,555, and named the experiment: stub the graph build, re-time. --help-task IS that arm (returns before buildGraph on the same lean cache blob): on llvm-project warm it takes 3.8 s where --grep takes 159.7 s and --callers=main 152.9 s, all at ~5.9 GB RSS. The floor is the graph; the cache load and per-file validation are linear (16 us/file on llvm vs 28 on go); memory is the ingest's tables and does not move with the graph. Profiled to one operation. New PROFILE_SCOPE scopes on the ingest path (parse-pool join + merge, the two corpus-wide model post-passes) and inside buildGraph (the resolve loop and the five passes after it); a scratch six-span split inside the loop put 145.1 s of 153.2 s in "CHA-lite cone + arity + locality", and a second split put arity at 0.015 s and locality at 0.27 s. The cone — {type} ∪ ancestors ∪ descendants over the class-name graph, two BFS walks with an O(n²) std::find dedup, capped at 4096 — was rebuilt for every still-ambiguous receiver-typed call: 86,667 rebuilds for 2,984 distinct receiver types, mean cone 1,075 names, 1.65 ms each. The obvious suspect, the five linear passes over same-name candidates, visited 1.23 billion candidates and cost 3.3 s. ChaConeMemo (src/graph.h) computes each receiver type's cone once, over class names interned in byte-sorted order, with the per-call walk's exact seed, discovery order and outer-loop-only cap, and answers membership by binary search. Warm llvm: --grep 159.7 s -> 9.2 s, --callers 152.9 s -> 8.6 s, the default map 248 s -> 10 s; --help-task unchanged. Default maps at --top-k=100000 are byte-identical pre/post on go (10,415,057 B) and llvm (21,802,319 B). Gate test/chaconecheck.sh (+ test/chaconefix/): the cone keyed on the receiver type not the callee (Dog and Cat on one speak), the memo hit from a second file, a receiver with no inheritance facts degrading rather than emptying the tier, a parameter-receiver control, and the cap's own shape on a generated 4,097-class corpus (B never expanded, B1::m outside the cone: count=4095, amb="1"). Every arm passes against the pre-fix binary. Gate count 564 -> 565 at every spelled site. Ledger in bench/PROFILE.md, the evidence chain in docs/EVALS.md, a CHANGELOG entry. No timing assertion anywhere. Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 11 ++ README.md | 4 +- bench/PROFILE.md | 96 +++++++++++++++ docs/EVALS.md | 80 ++++++++++++- present/deck5_ripwire_build.js | 6 +- src/graph.h | 205 +++++++++++++++++++++++++-------- src/ingest.cpp | 10 +- src/ingest_parsepool.h | 4 + test/chaconecheck.sh | 112 ++++++++++++++++++ test/chaconefix/a.cpp | 4 + test/chaconefix/b.cpp | 8 ++ test/chaconefix/zoo.h | 36 ++++++ test/regression.sh | 2 +- 13 files changed, 522 insertions(+), 56 deletions(-) create mode 100755 test/chaconecheck.sh create mode 100644 test/chaconefix/a.cpp create mode 100644 test/chaconefix/b.cpp create mode 100644 test/chaconefix/zoo.h diff --git a/CHANGELOG.md b/CHANGELOG.md index b5aa5e0b..f500c9ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,17 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — the super-linear warm floor under every graph-building verb (`--grep`, `--callers`, the map) + +On llvm-project (182,555 files, warm cache) a `--grep` for an absent literal took ~157 s, `--callers=main` +~152 s and the default map 248 s, while the same crawl + cache load + model build without the graph took +3.8 s. Profiled to one operation: the resolver rebuilt a receiver type's inheritance cone (two BFS walks +with quadratic dedup) on every still-ambiguous receiver-typed call — 86,667 rebuilds for 2,984 distinct +types, 143 s of the 154 s run. `ChaConeMemo` (`src/graph.h`) computes each cone once with the identical +walk and cap; warm `--grep` is now 9 s, `--callers` 8.6 s, the map 10 s, and default maps are byte-identical +before and after on go and llvm. Gate `test/chaconecheck.sh`; the phase tables are in `bench/PROFILE.md` +and the evidence chain in `docs/EVALS.md` (2026-09-09). + ### Added — a Ruby constant receiver is a dependency (parser version 83) Round two of the Ruby constant work. Parser version 82 gave the declarative spellings — `class X < Base`, diff --git a/README.md b/README.md index e7742d36..81b95cf4 100644 --- a/README.md +++ b/README.md @@ -1800,9 +1800,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-566 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +567 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **566 gate scripts** and is the authoritative list; +`test/regression.sh` names **567 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/bench/PROFILE.md b/bench/PROFILE.md index 12933c46..d4268b87 100644 --- a/bench/PROFILE.md +++ b/bench/PROFILE.md @@ -1077,3 +1077,99 @@ done Name the subtree `ext/`, not `vendor/` — the crawl's taxonomy filter skips a directory called `vendor` outright, so a corpus built under that name silently measures 1,000 files in both arms. + +## 2026-09-09 — the super-linear warm `--grep` floor: one stage, one operation, 143 s of 154 s on llvm-project + +LEDGER row, never a gate (the no-perf-budget rule). The correctness gate this round landed is +`test/chaconecheck.sh`; it asserts sets, never seconds. The question came from the tgrep head-to-head +(docs/EVALS.md, "Head-to-head vs tgrep"): warm `--grep` cost 40 µs/file at 2,240 and 15,865 files and +938 µs/file at 182,555, and the decisive experiment named there was "stub the graph build, re-time". + +### Instrument and argv + +One binary, `cmake -S . -B build_prof -DRIPWIRE_PROFILE=ON` (plain flags otherwise — never Release, `NDEBUG` +compiles `DEGRADED_PATH_ALERT` and the profiler out). This round first added the scopes the ingest path +lacked at the grep path's granularity: `buildGraph/3..8` (the resolve loop and the five passes after it), +the parse pool's worker join and per-thread merge, and the two corpus-wide model post-passes. The +per-reference split inside the loop (six span timers + volume counters) was a scratch patch, not landed. + +Corpora by `rg --files`: `golang/go` `49c3ea64` 15,865 files; `llvm/llvm-project` `2061c237` (shallow) +182,555 files, 2.9 GB — the same checkouts the head-to-head used. Warm = a dedicated `TMPDIR` per corpus, +primed once (llvm cold prime: 231 s wall, 367 s user on 18 cores, peak RSS 6.49 GB). Arms interleaved +`grep, help, callers` × 2 reps; `/usr/bin/time -l` for wall + RSS. 18-core Apple Silicon, 48 GB, shared: +the 1-minute load ranged 2.3–26 across the session, so read the ratios, not the third digit. + +``` +TMPDIR= build_prof/ripwire --grep=zzqxvnotpresentzz # the absent literal (full scan) +TMPDIR= build_prof/ripwire --callers=main # same graph, NO text scan +TMPDIR= build_prof/ripwire --help-task=zzqxvnotpresentzz # crawl + cache + model, NO graph +``` + +`--help-task` returns before `buildGraph` and uses the same lean cache blob as `--grep`, so it IS the +"graph stubbed out" arm the head-to-head asked for, with no code change. + +### Result — the decisive experiment, then the profile, then the operation + +| llvm-project, warm, wall | pre-fix (2 reps) | **post-fix (2 reps)** | +| --- | --- | --- | +| `--grep=` | 159.7 s, 153.9 s | **9.2 s, 9.0 s** | +| `--callers=main` | 152.9 s, 151.8 s | **8.6 s, 8.7 s** | +| `--help-task` (no graph) | 3.8 s, 3.4 s | 3.7 s, 3.4 s | +| default map, `--top-k=100000` | 248 s | **10 s** | +| peak RSS, any arm | 5.9–6.2 GB | 5.8–5.9 GB | + +**The floor is the graph, and only the graph.** The arm that runs the identical crawl, cache load, +validation and model build but never builds the graph took 3.8 s at the same 5.9 GB RSS. The cache load +plus per-file validation is 16 µs/file on llvm against 28 µs/file on go — linear, if anything sub-linear. +The memory-cliff hypothesis is refuted by the same row: RSS is the ingest's reference tables, present with +and without the graph, and wall did not move with it. + +Profile scopes, llvm warm `--grep`, pre-fix (rep 1, 159.7 s wall): `buildGraph` 154.6 s, of which the +per-reference resolve loop 153.2 s; `ingest: total` 4.7 s (crawl 1.8 incl. the git ignore probe 1.3, +model 1.2, parse pool 1.2, loadCache 0.4); `grep/1 grepCollect scan` 1.07 s on its own thread. The 2.9 GB +text scan is 1 s; the answer waited 153 s for the graph. + +Inside the loop, `--callers=main` warm, six spans over the SAME 4,546,850 references: + +| span (per reference) | calls | total | mean | +| --- | ---: | ---: | ---: | +| a: role filter + byName + SCIP/binding tiers | 5,162,745 | 0.11 s | 0.02 µs | +| b: canonical + L3 + ES import + rules 1/2/2b/2c/3 | 4,546,850 | 1.09 s | 0.24 µs | +| c: external veto + candidate spray + namespace gate | 4,517,099 | 2.18 s | 0.48 µs | +| d: tier ladder (same file / same dir / unique) | 4,137,640 | 1.07 s | 0.26 µs | +| **e: CHA-lite cone + arity + locality** | 2,213,632 | **145.1 s** | **65.5 µs** (max 41.7 ms) | +| f: amb + confidence + edge emission | 2,213,632 | 0.31 s | 0.14 µs | + +The obvious suspect was innocent: the five linear passes over the same-name candidate list visited +1,226,680,236 candidates (`test` 502 M of them, 7,405 defs; `S` 291 M; `foo` 76 M) and cost 3.3 s in +total — ~3 ns a visit, sequential ids, the prefetcher's happy case. Splitting span e once more: arity +0.015 s, the S6-C locality tie-break 0.27 s, **the CHA-lite cone ≈ 143 s.** + +**The operation.** For every still-ambiguous call with a known receiver static type, the loop rebuilt the +type's inheritance cone — two BFS walks over the class-NAME graph, `std::vector` with an +O(n²) `std::find` dedup, capped at 4,096 per walk — and tested each tier candidate by another linear +`std::find`. Counters: **86,667 cones for 2,984 distinct receiver types** (each rebuilt ~29×), mean cone +1,075 names (Σ 93,185,627), 1.65 ms a cone. On go the same span is 1.1 ms total — zero cones, because the +model has no class-inheritance edges there — which is why the floor looked flat until the corpus had deep +hierarchies. That is the super-linearity: Σ over calls of (cone size)², where both factors grow with the +tree. + +**The fix** (`src/graph.h`, `ChaConeMemo`): one cone per receiver type, computed on first use over class +names interned to dense ids in byte-sorted order, the walk verbatim (same seed, discovery order and the +outer-loop-only cap), membership by binary search. Post-fix the loop is 4.5 s; span c's 2.2 s of +candidate spray is now the largest remaining item and is a different, linear-per-candidate fix. Default +maps are **byte-identical** pre/post on both corpora (go 10,415,057 B; llvm 21,802,319 B) and the six +resolver gates plus 18 more pass unchanged. Per file, the warm `--grep` floor is now 50 µs at 182,555 +files against 33 µs at 15,865 — the 24× per-file regression is 1.5×. + +### Reproduce + +``` +cmake -S . -B build_prof -DRIPWIRE_PROFILE=ON && cmake --build build_prof -j +git clone --depth 1 https://github.com/llvm/llvm-project /llvm-project # ~2.9 GB, ~182,555 files +export TMPDIR=/tmp-llvm; mkdir -p "$TMPDIR" +build_prof/ripwire /llvm-project --grep=zzqxvnotpresentzz >/dev/null 2>prime.err # cold prime +for rep in 1 2; do for arm in --grep=zzqxvnotpresentzz --help-task=zzq --callers=main; do + /usr/bin/time -l build_prof/ripwire /llvm-project "$arm" >/dev/null 2>"$arm.$rep.err"; done; done +# the stderr report's "hottest scopes" table is the phase split; buildGraph/3 is the resolve loop +``` diff --git a/docs/EVALS.md b/docs/EVALS.md index b461fe09..6fa41e24 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 566 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 567 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5579,7 +5579,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **566 gate scripts**, all of which exist on disk. +naming **567 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -6491,7 +6491,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 566. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 567. The loop is the authority; the stale docstrings are a known drift. `test/manifestcheck.sh` asserts this very number against the loop's actual length, so it cannot go stale silently again. - **"282 argv vectors."** The gate asserts a floor of ≥250 assembled from five sources; 282 was a @@ -13007,3 +13007,77 @@ dumps that the generated-document demotion does not catch (no marker, no fences `classifyGeneratedDoc` states about itself). `.txt` stays prose to every reader-facing lens and an unindexed extension in `unindexed=`, alongside `.log`, `.lock` and `.out`. An evidence-based admission test that reads BYTES rather than the extension is the open follow-up. + +## The super-linear warm `--grep` floor: measured to one operation, fixed, byte-identical (2026-09-09) + +The tgrep head-to-head of 2026-09-09 (its section "Head-to-head vs tgrep (microsoft/tgrep 1.0.5)" lands +with the harvest-tgrep lane) left one item open: warm `--grep` cost 40.2 µs/file at 2,240 files, +39.1 µs at 15,865 and 937.8 µs at 182,555 — a 24× per-file regression across an 11.5× corpus step — and +it named the decisive experiment: time the verb with the graph construction stubbed out. This section +ran that experiment, then followed the house perf reflex (start from the measurement, inspect only the +symbols the profile names). The full phase tables and the reproduce block are in `bench/PROFILE.md` +("2026-09-09 — the super-linear warm `--grep` floor"); this is the evidence chain and the verdict. + +**The stub, without a stub.** `--help-task` returns before `buildGraph` and shares `--grep`'s lean cache +blob, so it is the crawl + cache-load + validation + model-build arm with the graph removed. Warm on +llvm-project (182,555 files, same checkout as the head-to-head), interleaved, two reps each: + +| arm | wall | peak RSS | +| --- | ---: | ---: | +| `--grep=` | 159.7 s, 153.9 s | 6.15 GB | +| `--callers=main` (graph, no scan) | 152.9 s, 151.8 s | 5.91 GB | +| `--help-task` (no graph) | 3.8 s, 3.4 s | 5.93 GB | + +The floor did move — by 150 s. The cost is the graph; the cache load plus per-file validation is +16 µs/file on llvm against 28 µs/file on go, linear; and the memory-cliff hypothesis is refuted on the +same row, since the 5.9 GB is the ingest's own tables and is present in the arm that takes 3.8 s. + +**Which operation.** The ingest path already carried `PROFILE_SCOPE_DESCRIBE` scopes at the grep path's +granularity for crawl, cache and model; `buildGraph` carried one scope for the whole function. This round +added the loop and post-loop scopes that land, plus a scratch six-span split inside the per-reference +loop. Of 153.2 s in the loop, 145.1 s sat in ONE span — CHA-lite cone + arity + locality — across +2,213,632 references at a mean 65.5 µs and a worst case of 41.7 ms. Split again: arity 0.015 s, locality +0.27 s, **the CHA-lite cone ≈ 143 s**. The obvious hypothesis was measured and rejected: the five linear +passes over same-name candidates visited 1.23 billion candidates and cost 3.3 s. + +The cone was recomputed per call: 86,667 BFS pairs for 2,984 distinct receiver types (≈29 rebuilds each), +mean cone 1,075 class names, quadratic `std::find` dedup, 1.65 ms a cone. On go the same span is 1.1 ms +in total because the model has no inheritance edges there — the flat rungs of the ladder were flat because +the corpora had no deep hierarchies, not because the code was linear. + +**The fix and its proof.** `ChaConeMemo` (`src/graph.h`) computes each receiver type's cone once, over +interned class names, with the per-call walk's exact seed, discovery order and 4,096 outer-loop cap, and +answers membership by binary search. Warm llvm: `--grep` 9.2 s / 9.0 s, `--callers` 8.6 s / 8.7 s, the +default map 248 s → 10 s; `--help-task` unchanged. Default maps at `--top-k=100000` are byte-identical +pre/post on go (10,415,057 B) and llvm (21,802,319 B). Gate `test/chaconecheck.sh` pins the set the memo +must reproduce: a cone keyed on the receiver type and not the callee (Dog and Cat on one `speak`), the +memo-hit path from a second file, a receiver with no inheritance facts degrading rather than emptying the +tier, a parameter-receiver control, and the cap's own shape (Base→{A,B}, A→A1..A4095, B→B1: B is never +expanded, so B1::m is outside the cone; count=4095, amb="1"). All arms pass against the pre-fix binary — +the expected values are the per-call walk's own answers — and 24 existing resolver gates pass unchanged. + +**What remains, stated as a floor.** Warm `--grep` on llvm is 9 s: `buildGraph` 5.9 s (the resolve loop +4.5 s, of which the candidate spray over 1.23 billion visits is 2.2 s), ingest 2.8 s, the 2.9 GB scan +1.0 s on its own thread. Per file that is 50 µs at 182,555 files against 33 µs at 15,865 — 1.5×, not 24×. +`rg` answers the same absent literal in 4.3 s; a resident tgrep in 0.018 s. The next rung is the linear +candidate passes, which is a different design (a per-name file/directory index) and is not started here. +The cold parse on llvm carried the same ~186 s cone cost inside its 231 s and was not re-measured. + +**The head-to-head's top rung, re-timed post-fix.** The same six frozen queries the lane declared for the +llvm rung, the same argv (`--grep-in=any`), the plain build with the memo, warm, three reps each, medians: + +| query | ripwire warm, head-to-head (pre-fix) | **ripwire warm, post-fix** | +| --- | ---: | ---: | +| L1 `pthread_mutex_lock` | 193.09 s | **8.99 s** | +| L3 `TODO` | 195.58 s | **9.00 s** | +| L6 `zzqxvnotpresentzz` (absent) | 176.35 s | **9.20 s** | +| R3 `malloc.*free` | 215.34 s | **9.23 s** | +| R4 `[Qq]z[Xx]v.*[Jj]w` (prefilter-defeating) | 171.19 s | **9.11 s** | +| R1 `^#include` | 233.06 s | 10.08 s — not comparable: this branch predates the lane's line-anchor fix | + +Every query is now within a second of the absent literal: the scan is still hidden behind the graph, the +graph is just 17× smaller. Re-deriving the lane's Q\* formula with its own tgrep numbers (B = 11.113 s, +q_index = 2.7513 s over the same six) and a post-fix q_scan of ≈ 9.3 s gives **Q\* ≈ 1.7 queries against +ripwire-warm** where the lane read 0.1 — the resident index still pays for itself inside a two-query +session at this scale, but no longer "before the first query finishes". tgrep itself was not re-run; only +the ripwire column moved. diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index b0a1b7ca..05f321aa 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -708,7 +708,7 @@ function row(s, y, h, cols, opts={}){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["566 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], + ["567 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -732,7 +732,7 @@ function row(s, y, h, cols, opts={}){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "566", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", + stat(s, "567", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -971,7 +971,7 @@ function row(s, y, h, cols, opts={}){ ["179 long flags · 29 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["566 gate scripts", "bash test/manifestcheck.sh"], + ["567 gate scripts", "bash test/manifestcheck.sh"], ["43 repos · 69 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/graph.h b/src/graph.h index 44c6dafa..c3127253 100644 --- a/src/graph.h +++ b/src/graph.h @@ -547,6 +547,141 @@ inline bool keepRustQualifiedCandidates( const IngestResult& ing, const HashMap< return true; } +// ── B2.1 CHA-lite cone memo (perf round 2026-09-09: the super-linear warm --grep floor) ───────────────── +// A receiver type's inheritance CONE — {type} ∪ transitive ancestors ∪ transitive descendants over the +// class-NAME graph — is a pure function of the type name once chaUp/chaDown are built, yet the resolve loop +// recomputed it on EVERY still-ambiguous receiver-typed call: two BFS walks with an O(n²) std::find dedup +// over std::string. Measured warm on llvm-project (182,555 files): 86,667 walks for 2,984 distinct receiver +// types, mean cone 1,075 names, 1.65 ms each — 143 s of a 154 s --callers/--grep run, the whole of the +// floor docs/EVALS.md's tgrep head-to-head left open (bench/PROFILE.md carries the phase table). +// +// The memo computes each cone ONCE. Class names are interned to dense ids assigned in byte-sorted name order, +// the walk is the per-call walk verbatim — same seed, same discovery order, same `out.size() < kChaConeCap` +// test at the OUTER loop only (the adjacency list that crosses the cap is pushed whole; nothing after it is +// expanded) — and membership is a binary search, so the set is exactly the one the per-call walk produced. +// test/chaconecheck.sh pins that, arm 5 being the cap's own shape. A receiver type with no inheritance facts +// is not interned at all: its cone is {itself}, answered by string equality, as the per-call walk did. +struct ChaConeMemo +{ + static constexpr std::size_t kChaConeCap = 4096; // per-walk discovery cap, unchanged from the per-call walk + static constexpr std::uint32_t kNoCone = 0xFFFFFFFFu; + + // A cone handle: `ids == nullptr` ⇒ the receiver type has no inheritance facts (membership is equality with + // the type itself). Valid until the next coneFor() call — cones live in a growing vector. + struct Cone + { + const std::vector* ids; + }; + + ChaConeMemo( const HashMap>& chaUp, + const HashMap>& chaDown ) + { + for( const auto& [ k, v ] : chaUp ) + { + names_.push_back( k ); + names_.insert( names_.end(), v.begin(), v.end() ); + } + for( const auto& [ k, v ] : chaDown ) + { + names_.push_back( k ); + names_.insert( names_.end(), v.begin(), v.end() ); + } + std::sort( names_.begin(), names_.end() ); + names_.erase( std::unique( names_.begin(), names_.end() ), names_.end() ); + idOf_.reserve( names_.size() ); + for( std::uint32_t i = 0; i < names_.size(); ++i ) + { + idOf_.emplace( names_[ i ], i ); + } + up_.assign( names_.size(), {} ); + down_.assign( names_.size(), {} ); + fillAdjacency( chaUp, up_ ); + fillAdjacency( chaDown, down_ ); + stamp_.assign( names_.size(), 0 ); + coneIndex_.assign( names_.size(), kNoCone ); + } + + Cone coneFor( std::string_view recvType ) + { + key_.assign( recvType ); + const auto rit = idOf_.find( key_ ); + if( rit == idOf_.end() ) + { + return Cone{ nullptr }; + } + const std::uint32_t root = rit->second; + if( coneIndex_[ root ] == kNoCone ) + { + walk( up_, root, upScratch_ ); // {recvType} ∪ ancestors + walk( down_, root, downScratch_ ); // {recvType} ∪ descendants — a SEPARATE walk, never chaDown out of an ancestor + std::vector cone( upScratch_ ); + cone.insert( cone.end(), downScratch_.begin(), downScratch_.end() ); + std::sort( cone.begin(), cone.end() ); + cone.erase( std::unique( cone.begin(), cone.end() ), cone.end() ); + coneIndex_[ root ] = std::uint32_t( cones_.size() ); + cones_.push_back( std::move( cone ) ); + } + return Cone{ &cones_[ coneIndex_[ root ] ] }; + } + + // Is a candidate's enclosing scope inside `cone`? A scope no inheritance fact ever named cannot be in any + // interned cone; a scope-less free function is never a member-call target and is correctly excluded. + bool contains( Cone cone, std::string_view recvType, const std::string& scope ) const + { + if( cone.ids == nullptr ) + { + return scope == recvType; + } + const auto it = idOf_.find( scope ); + return it != idOf_.end() && std::binary_search( cone.ids->begin(), cone.ids->end(), it->second ); + } + +private: + void fillAdjacency( const HashMap>& adj, std::vector>& out ) const + { + for( const auto& [ k, v ] : adj ) + { + std::vector& row = out[ idOf_.find( k )->second ]; + row.reserve( v.size() ); + for( const std::string& nm : v ) // v is sorted+unique (built so in buildGraph) ⇒ ascending ids, the per-call order + { + row.push_back( idOf_.find( nm )->second ); + } + } + } + + // The per-call walk, over ids: seeded at `root`, discovery order = adjacency order, dedup by epoch stamp + // (== the old `std::find( out, nm ) == out.end()`), capped at the OUTER loop exactly as before. + void walk( const std::vector>& adj, std::uint32_t root, std::vector& out ) + { + ++epoch_; + out.clear(); + out.push_back( root ); + stamp_[ root ] = epoch_; + for( std::size_t qi = 0; qi < out.size() && out.size() < kChaConeCap; ++qi ) + { + for( const std::uint32_t nb : adj[ out[ qi ] ] ) + { + if( stamp_[ nb ] != epoch_ ) + { + stamp_[ nb ] = epoch_; + out.push_back( nb ); + } + } + } + } + + std::vector names_; // interned class names, byte-sorted (id = index) + HashMap idOf_; + std::vector> up_, down_; // adjacency by id, rows in sorted-name order + std::vector stamp_; // epoch-stamped visited set for walk() + std::uint32_t epoch_ = 0; + std::vector coneIndex_; // root id → index into cones_, or kNoCone + std::vector> cones_; // sorted id sets, one per computed receiver type + std::vector upScratch_, downScratch_; + std::string key_; // reused lookup buffer (no per-call allocation) +}; + // THE TIER-3 CANONICAL RESCUE (H4 V3 M-3) — why `canonical` sits beside `narrowed` in buildGraph's tier-3 // gate. Tier 3 is "a UNIQUE global, else DROP". A Rule-1 narrowed call has always been exempt, because it is // pinned to ONE scope and is therefore resolved rather than guessed. A CANONICAL hit is pinned in exactly the @@ -1699,9 +1834,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null for( auto& [ k, v ] : chaUp ) { std::sort( v.begin(), v.end() ); v.erase( std::unique( v.begin(), v.end() ), v.end() ); } for( auto& [ k, v ] : chaDown ) { std::sort( v.begin(), v.end() ); v.erase( std::unique( v.begin(), v.end() ), v.end() ); } } - std::vector chaAllowed; // reused per-call cone (allowed class-name set) buffer - std::vector chaDesc; // reused per-call descendants-closure scratch (kept separate from the - // ancestors walk so following one direction can never leak siblings) + ChaConeMemo chaCones( chaUp, chaDown ); // one cone per receiver type, computed on first use (see the type) std::vector filtScratch; // reused per-call survivor buffer for CHA-lite / arity filtering // ---- census arming + the ORACLE side (eval-only; src/pincensus.h) ------------------------------ @@ -1735,6 +1868,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.pinCensus.addOracleRow( nd.from, nd.calleeName, nd.kind ); } } + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/3: resolve loop (per reference)" ); for( const Reference& r : ing.references ) { // file-scope / inheritance / doc-mention / HAS-A → not a call. ABS-3: read/write/import use-sites @@ -2202,51 +2337,17 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const std::string_view recvType = narrower.receiverStaticType( r, ing.symbols[ r.fromSymbol ].scope ); if( !recvType.empty() ) { - // Build the strict cone = {recvType} ∪ ANCESTORS ∪ DESCENDANTS via TWO fully-independent - // directional closures, each seeded ONLY at recvType. A value/pointer of static type T is - // dynamically T or a subtype, so its target is either an override in T / a subtype OR the - // definition T inherits from an ancestor — but NEVER a sibling's method (a T can never BE a - // sibling). Keeping the closures separate (never following chaDown out of an ancestor) is - // what excludes siblings/cousins for precision, while still never dropping a true target. - // Directional BFS into `out`, seeded at recvType. Index by a COPIED key — push_back may - // reallocate `out`, so a held reference would dangle. - const auto closure = [ & ]( const HashMap>& adj, - std::vector& out ) - { - out.clear(); - out.emplace_back( recvType ); - for( std::size_t qi = 0; qi < out.size() && out.size() < 4096; ++qi ) - { - const std::string cur = out[ qi ]; - const auto it = adj.find( cur ); - if( it == adj.end() ) - { - continue; - } - for( const std::string& nm : it->second ) - { - if( std::find( out.begin(), out.end(), nm ) == out.end() ) - { - out.push_back( nm ); - } - } - } - }; - closure( chaUp, chaAllowed ); // {recvType} ∪ ancestors - closure( chaDown, chaDesc ); // {recvType} ∪ descendants - for( const std::string& nm : chaDesc ) - { // merge descendants into the allowed cone (dedup) - if( std::find( chaAllowed.begin(), chaAllowed.end(), nm ) == chaAllowed.end() ) - { - chaAllowed.push_back( nm ); - } - } - // keep only candidates whose enclosing class name is in the cone (a scope-less free function - // is not a member-call target ⇒ correctly excluded). Degrade if the intersection is empty. + // The strict cone = {recvType} ∪ ANCESTORS ∪ DESCENDANTS, two fully-independent directional + // walks each seeded ONLY at recvType (never chaDown out of an ancestor, so a sibling's method — + // which a T can never BE — is excluded while a true target never is). Computed once per receiver + // type by ChaConeMemo (its header carries the measurement that moved it out of this loop) and + // answered here by membership: keep only candidates whose enclosing class name is in the cone (a + // scope-less free function is not a member-call target ⇒ correctly excluded). Degrade if empty. + const ChaConeMemo::Cone cone = chaCones.coneFor( recvType ); filtScratch.clear(); for( NodeId c : tier ) { - if( std::find( chaAllowed.begin(), chaAllowed.end(), ing.symbols[ c ].scope ) != chaAllowed.end() ) + if( chaCones.contains( cone, recvType, ing.symbols[ c ].scope ) ) { filtScratch.push_back( c ); } @@ -2507,7 +2608,10 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null } } } + } + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/4: flatten edges + out/in CSR" ); // flatten + cap + sort by (from, to) — deterministic regardless of map order struct E { NodeId from, to; float w; }; std::vector edges; @@ -2596,6 +2700,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null for( const E& e : edges ) { const std::uint32_t pos = cur[ e.to ]++; ci[ pos ] = e.from; val[ pos ] = e.w; } } VERIFY( verifyCsr( g.inEdges, N ) ); + } // inheritance edges (Lego view): isInherit refs (derived → base name) → implementors[base] += derived. // Resolve the base name to class-like symbols (any-file, by name); dedup. The socket→bricks relation. @@ -2604,6 +2709,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // than restated, because a second copy of one rule is how the two copies end up disagreeing. const auto isClassLike = []( SymKind k ) noexcept { return namespaceCompatible( RefRole::Extends, k ); }; + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/5: inheritance edges" ); for( const Reference& r : ing.references ) { if( !r.isInherit ) @@ -2668,6 +2775,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.implementors[ baseId ].push_back( derived ); } } + } for( std::vector& v : g.implementors ) { std::sort( v.begin(), v.end() ); @@ -2678,6 +2786,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // Resolve the name to real DEFINITIONS (body present), any file; stored OUT of the call graph so a doc // mentioning a symbol never inflates its PageRank / blast radius. "what docs discuss this symbol". g.mentions.assign( N, {} ); + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/6: doc mentions" ); for( const Reference& r : ing.references ) { if( !r.isDocLink || r.fromSymbol == kNoNode ) @@ -2706,6 +2816,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.mentions[ def ].push_back( r.fromSymbol ); } } + } for( std::vector& v : g.mentions ) { std::sort( v.begin(), v.end() ); @@ -2715,6 +2826,8 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // S5-E HAS-A composition edges: isCompose refs (owner class → member type name) → composeEdges. // Resolve the type name to class/struct symbols (any-file, by name); store OUTSIDE the call graph so // PageRank, ranks, and the default map are UNCHANGED. Sorted (ownerSym, typeSym) for determinism. + { + PROFILE_SCOPE_DESCRIBE( "buildGraph/7: HAS-A compose edges" ); for( const Reference& r : ing.references ) { if( !r.isCompose || r.fromSymbol == kNoNode ) @@ -2766,6 +2879,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.composeEdges.push_back( std::move( ce ) ); break; } } + } // sort by (ownerSym, typeSym, fieldName) for determinism; dedup on (ownerSym, fieldName) — the // type name is the primary identity (one field has exactly one declared type). std::sort( g.composeEdges.begin(), g.composeEdges.end(), @@ -2796,6 +2910,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // extends resolvers above use. Ambiguous USEs (matching ≥2 DISTINCT resolved handlers) and unresolved // USEs (matching zero) synthesize NO edge — never a guess (mirrors the amb=/unresolved= honesty posture). { + PROFILE_SCOPE_DESCRIBE( "buildGraph/8: HTTP-route edges" ); const auto isFunctionLike = []( SymKind k ) noexcept { return k == SymKind::Function || k == SymKind::Method; }; std::vector defHandler( ing.routeDefs.size(), kNoNode ); // per-DEF resolved handler symbol diff --git a/src/ingest.cpp b/src/ingest.cpp index 99b4c2ac..8dfef6b4 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -325,7 +325,10 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud // macro-edges round: the corpus-wide role="macro" retag (model.h). AFTER the model is assembled and // AFTER saveCache (which stores the per-file truth, role=Call) — a #define added in one file must // re-judge every OTHER file's cached call sites on the next run, so the retag can never be persisted. - retagMacroCallReferences( result ); + { + PROFILE_SCOPE_DESCRIBE( "ingest/build-model: macro retag (corpus-wide post-pass)" ); + retagMacroCallReferences( result ); + } // r9 shadow suppression (model.h): a reference inside a function whose LOCAL declarations bind the same // name as a variable belongs to the local, not to any same-named indexed symbol — erase it here, the one @@ -334,7 +337,10 @@ IngestResult ingest( const char* rootDir, const std::vector& exclud // macro retag (role="macro" is preprocessor evidence and stays) and AFTER saveCache (per-file truth is // persisted unsuppressed; the collision gate depends on the whole corpus' symbols, so the judgment can // never be cached per-file — same reasoning as the retag above). - suppressShadowedReferences( result ); + { + PROFILE_SCOPE_DESCRIBE( "ingest/build-model: shadow suppression (r9 post-pass)" ); + suppressShadowedReferences( result ); + } return result; } diff --git a/src/ingest_parsepool.h b/src/ingest_parsepool.h index 9e6c995e..35309276 100644 --- a/src/ingest_parsepool.h +++ b/src/ingest_parsepool.h @@ -518,6 +518,7 @@ inline void runParseWorker( ParsePoolShared& sh, unsigned t ) // re-sorted by the model-build tail), reserving each family's exact total first. inline RawFacts mergeThreadFacts( std::vector& tFacts ) { + PROFILE_SCOPE_DESCRIBE( "ingest/parse-pool: merge per-thread facts" ); RawFacts raw; std::size_t totDefs = 0, totRefs = 0, totIncs = 0, totBinds = 0, totFfis = 0, totRouteDefs = 0, totRouteUses = 0, totConstOpens = 0; for( const RawFacts& tf : tFacts ) @@ -755,10 +756,13 @@ inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::st installCompiledQueriesAndOpenGate( prewarm ); // join async compiles, publish, open the gate (ingest_prewarm.h) + { + PROFILE_SCOPE_DESCRIBE( "ingest/parse-pool: workers run + join" ); for( std::thread& th : pool ) { th.join(); } + } raw = mergeThreadFacts( tFacts ); diff --git a/test/chaconecheck.sh b/test/chaconecheck.sh new file mode 100755 index 00000000..60a10c5e --- /dev/null +++ b/test/chaconecheck.sh @@ -0,0 +1,112 @@ +#!/usr/bin/env bash +# chaconecheck.sh — gate for the B2.1 CHA-lite cone MEMO (perf round 2026-09-09, the super-linear warm +# --grep floor): graph.h::buildGraph used to recompute a receiver type's inheritance cone — {type} ∪ +# ancestors ∪ descendants, two BFS walks over the class-NAME graph with an O(n²) std::find dedup and a +# 4096-entry cap per walk — on EVERY still-ambiguous receiver-typed call. Measured on llvm-project +# (182,555 files, warm): 86,667 cones for 2,984 distinct receiver types, mean cone 1,075 names, +# 1.65 ms each = 143 s of a 154 s --callers/--grep run (bench/PROFILE.md has the phase table). The fix +# computes each receiver type's cone ONCE and answers membership from it. +# +# WHAT THIS GATE PINS: that the memoised cone is the SAME SET the per-call walk produced — including the +# two behaviours a re-implementation is most likely to change silently: +# (a) the cone is keyed on the RECEIVER TYPE, not the callee name: g1/g2 (Dog, two files) and g3 (Cat) +# call the same `speak` and must get their OWN cones; a memo keyed on the callee would hand Cat the +# Dog answer (both contain Animal — so arm 3 pins the *Lamp* case too, where the answers differ); +# (b) the BFS cap is `out.size() < 4096` checked at the OUTER loop only: the adjacency list that crosses +# the cap is pushed whole, and nothing AFTER it is expanded. Arm 5 builds Base→{A,B}, A→A1..A4095, +# B→B1: Base's list [A,B] is expanded, A's 4095 children cross the cap, B is never expanded, so B1 is +# OUTSIDE the cone and B1::m is dropped while all 4095 A-children survive → count=4095, and the ONE +# call site is a 4095-way split → amb="1" (amb counts ambiguous CALLS, not tier width). +# A cap moved to the inner loop, or to 4097, or a dedup that changes discovery order, turns it red. +# Plus the degrade rule (a cone that keeps nothing leaves the tier untouched — g4), a control where the cone +# cannot fire (g5), determinism and XML well-formedness. +# +# RED-first proof (2026-09-09): against the pre-memo binary every arm PASSES (the expected values are the +# per-call walk's own answers, hand-derived above); mutating the cap to `< 4097` fails arm 5; keying the memo +# on the callee name fails arm 3. +# +# test/chaconecheck.sh # uses build/ripwire +# RIPWIRE_BIN=asan/ripwire test/chaconecheck.sh +# Exits non-zero on any failure; prints PASS/FAIL per check, ALL PASS on success. + +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +FIX="$ROOT/test/chaconefix" +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } + +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first (cmake --build build -j)"; exit 2; } +[ -d "$FIX" ] || { echo "no test/chaconefix dir — fixture missing"; exit 2; } +cd "$ROOT" + +echo "chaconecheck: BIN=$BIN CORPUS=test/chaconefix (+ a generated 4,097-class cap corpus)" + +# def lines derived from the source so the gate survives fixture edits +ANIMAL_LINE="$( grep -n 'inline void Animal::speak()' "$FIX/zoo.h" | cut -d: -f1 )" +ROBOT_LINE="$( grep -n 'void speak() { power' "$FIX/zoo.h" | cut -d: -f1 )" + +# distinct speak target lines a caller resolves to (--callees rows carry p="zoo.h:LINE") +targets(){ "$BIN" "$FIX" --callees="$1" --no-cache 2>/dev/null | tr '>' '\n' | grep -oE 'zoo\.h:[0-9]+' | grep -oE '[0-9]+$' | sort -un; } +count(){ "$BIN" "$FIX" --callees="$1" --no-cache 2>/dev/null | grep -oE ' count="[0-9]+"' | head -1 | grep -oE '[0-9]+'; } +hasamb(){ "$BIN" "$FIX" --no-cache --top-k=100000 2>/dev/null | grep -oE "n=\"$1\"[^>]*" | grep -q 'amb='; } + +# ── 1) the first Dog cone (a.cpp): Animal::speak ONLY, Robot dropped, no amb ──────────────────────────── +T="$( targets g1 )" +if [ "$( count g1 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && ! hasamb g1; then + ok "g1 (Dog, a.cpp): Animal::speak only (zoo.h:$ANIMAL_LINE), Robot dropped, no amb=" +else no "g1 (Dog, a.cpp): expected exactly Animal::speak — got count=$( count g1 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + +# ── 2) the memo HIT (b.cpp asks for the Dog cone again): byte-identical answer ────────────────────────── +T="$( targets g2 )" +if [ "$( count g2 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && ! hasamb g2; then + ok "g2 (Dog, b.cpp — memo hit): Animal::speak only, Robot dropped, no amb=" +else no "g2 (Dog, b.cpp): memo hit differs from g1 — got count=$( count g2 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + +# ── 3) a DIFFERENT cone on the same callee name (Cat), and one with NO inheritance facts (Lamp) ───────── +T="$( targets g3 )" +if [ "$( count g3 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! hasamb g3; then + ok "g3 (Cat): its own cone {Cat, Animal} → Animal::speak only, no amb=" +else no "g3 (Cat): expected exactly Animal::speak — got count=$( count g3 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi +T="$( targets g4 )" +if [ "$( count g4 )" = 2 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && hasamb g4; then + ok "g4 (Lamp): cone {Lamp} keeps nothing → DEGRADE, both targets kept, amb= honest" +else no "g4 (Lamp): expected the untouched 2-way split — got count=$( count g4 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + +# ── 4) control: a parameter receiver has no var→type binding, so no cone can fire ─────────────────────── +T="$( targets g5 )" +if [ "$( count g5 )" = 2 ] && hasamb g5; then + ok "g5 (Dog& parameter): receiver type unknown → 2-way split kept, amb= honest (control)" +else no "g5 (Dog& parameter): control should stay ambiguous — got count=$( count g5 )"; fi + +# ── 5) the 4096 BFS cap, reproduced exactly: Base→{A,B}, A→A1..A4095, B→B1; B is never expanded ───────── +CAP="$TMP/capfix"; mkdir -p "$CAP" +{ + printf 'struct Base { void driver() { this->m(); } };\n' + printf 'struct A : Base {};\n' + printf 'struct B : Base {};\n' + i=1; while [ "$i" -le 4095 ]; do printf 'struct A%d : A { void m() {} };\n' "$i"; i=$(( i + 1 )); done + printf 'struct B1 : B { void m() {} };\n' +} >"$CAP/cap.cpp" +B1_LINE="$( grep -n '^struct B1 : B' "$CAP/cap.cpp" | cut -d: -f1 )" +A4095_LINE="$( grep -n '^struct A4095 : A' "$CAP/cap.cpp" | cut -d: -f1 )" +OUT="$( "$BIN" "$CAP" --callees=driver --no-cache --limit=100000 2>/dev/null )" +CNT="$( printf '%s' "$OUT" | grep -oE ' count="[0-9]+"' | head -1 | grep -oE '[0-9]+' )" +LINES="$( printf '%s' "$OUT" | tr '>' '\n' | grep -oE 'cap\.cpp:[0-9]+' | grep -oE '[0-9]+$' | sort -un )" +AMB="$( "$BIN" "$CAP" --no-cache --top-k=100000 2>/dev/null | grep -oE 'n="driver"[^>]*' | grep -oE 'amb="[0-9]+"' | grep -oE '[0-9]+' )" +if [ "$CNT" = 4095 ] && printf '%s\n' "$LINES" | grep -qx "$A4095_LINE" && ! printf '%s\n' "$LINES" | grep -qx "$B1_LINE" && [ "${AMB:-0}" = 1 ]; then + ok "cap: Base::driver → A1..A4095::m (count=4095, one call amb=1); B1::m (cap.cpp:$B1_LINE) outside the capped cone" +else no "cap: expected count=4095 amb=1 without B1::m — got count=${CNT:-?} amb=${AMB:-none} B1_present=$( printf '%s\n' "$LINES" | grep -qx "$B1_LINE" && echo yes || echo no )"; fi + +# ── 6) determinism + well-formedness ──────────────────────────────────────────────────────────────────── +A="$( "$BIN" "$FIX" --callees=g2 --no-cache 2>/dev/null )"; B="$( "$BIN" "$FIX" --callees=g2 --no-cache 2>/dev/null )" +[ "$A" = "$B" ] && ok "determinism: --callees=g2 byte-identical run-to-run" || no "non-deterministic --callees output" +if command -v xmllint >/dev/null 2>&1; then + printf '%s' "$A" | xmllint --noout - 2>/dev/null && ok "xml well-formed" || no "xml malformed" +fi + +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit $fail diff --git a/test/chaconefix/a.cpp b/test/chaconefix/a.cpp new file mode 100644 index 00000000..debf69bb --- /dev/null +++ b/test/chaconefix/a.cpp @@ -0,0 +1,4 @@ +// chaconefix/a.cpp — the FIRST call whose receiver static type is Dog: the Dog cone is computed here. +#include "zoo.h" + +void g1() { Dog d; d.speak(); } // cone {Dog, Animal} → Animal::speak only; Robot::speak dropped diff --git a/test/chaconefix/b.cpp b/test/chaconefix/b.cpp new file mode 100644 index 00000000..1c0d27bb --- /dev/null +++ b/test/chaconefix/b.cpp @@ -0,0 +1,8 @@ +// chaconefix/b.cpp — the SECOND file to ask for the Dog cone (a memo HIT must equal the first answer), +// plus a different cone on the same callee name, a receiver with no inheritance facts, and a control. +#include "zoo.h" + +void g2() { Dog d; d.speak(); } // memo hit: byte-identical to g1's answer (Animal::speak only) +void g3() { Cat c; c.speak(); } // a DIFFERENT cone {Cat, Animal} keyed on the same callee `speak` +void g4() { Lamp l; l.speak(); } // cone {Lamp} keeps nothing → DEGRADE: tier untouched, stays ambiguous +void g5( Dog& p ) { p.speak(); } // control: parameter receiver → no var→type binding → cone cannot fire diff --git a/test/chaconefix/zoo.h b/test/chaconefix/zoo.h new file mode 100644 index 00000000..696605f0 --- /dev/null +++ b/test/chaconefix/zoo.h @@ -0,0 +1,36 @@ +// chaconefix/zoo.h — gate fixture for the CHA-lite cone memo (test/chaconecheck.sh). +// +// The same hierarchy shape as chafix/cha.cpp, kept in ONE header so every candidate for `speak` sits in +// the SAME directory as every caller (tier 2 of the ladder) and the tier reaches CHA-lite still ambiguous: +// Animal — a base with a bodied speak(); Dog and Cat implement it WITHOUT overriding speak(); +// Robot — UNRELATED, its own speak(); Lamp — a class with NO inheritance facts and NO speak(). +struct Animal +{ + void speak(); // declared here, defined out-of-line below (a real, bodied def) + virtual void move(); // an unrelated virtual so Animal is a genuine polymorphic base + int tag = 0; +}; + +struct Dog : Animal // implementor 1 — does NOT define speak (inherits Animal::speak) +{ + void move() override { tag = 1; } +}; + +struct Cat : Animal // implementor 2 — a DIFFERENT cone on the same callee name +{ + void move() override { tag = 2; } +}; + +struct Robot // UNRELATED — outside every Animal cone +{ + void speak() { power = 1; } + int power = 0; +}; + +struct Lamp // no bases, no derived classes, no speak(): its cone is {Lamp} alone +{ + int watts = 0; +}; + +inline void Animal::speak() { tag = 3; } +inline void Animal::move() { tag = 4; } diff --git a/test/regression.sh b/test/regression.sh index b1e03e39..e2b9c1c4 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -265,7 +265,7 @@ else RIPWIRE_BIN="$BIN" bash "$ROOT/test/codexdoctorcheck.sh" 2>&1 | sed 's/^/ | /' fi # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck candheadcheck candidatescheck canoncheck ccheck ccjsoncheck chacheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck atcheck atomscheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachesplitcheck callerscheck callformcheck candheadcheck candidatescheck canoncheck ccheck ccjsoncheck chacheck chaconecheck chainguardcheck chainidcheck churndecaycheck churnjoincheck churnjsonstampcheck clicheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck cppbenchcheck cppoperatorcheck cppqualcheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck deeptailcheck defaultceilingcheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check externalvetocheck fficheck fieldaffinitycheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnptrcheck forautobodycheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forlenscheck formatgatecheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gateexitcheck genrecallcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck graphlegendbudgetcheck graphqueryrefusecheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck headsnapcachecheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactimportcheck impactpartitioncheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck knownitemcheck landingcheck langcensuscheck langcheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legobundlecheck legocheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck manifestcheck mapdiffcheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstrictschemacheck mcptoolprunecheck mcptranchecheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck ordercheck outlinecheck overbudgetcommentcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pyshapecheck qackconcurrencycheck qackorigincheck qchurncheck qchurnmemocheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recalltablecheck recalltotalcheck receiptpostcheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyconstcheck rubymetricscheck rubyrecvcheck rubyrequirecheck rubyscopecheck rubysettercheck runhintcheck runtracecheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcontainedcheck shadowcheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck spectimingcheck staleackcheck statgatecheck sublistcountcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck tracecheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From 5c495525b60bc436afefc98d0d32f3ae8a76c679 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 12:49:08 -0400 Subject: [PATCH 2/8] =?UTF-8?q?fix(maxfilesizecheck):=20derive=20the=201M?= =?UTF-8?q?=20expectation=20from=20the=20tool's=20own=20rows=20=E2=80=94?= =?UTF-8?q?=20docs/EVALS.md=20crossed=201=20MiB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The arm asserted skipped_oversize == the count of tracked .json over the 256 K JSON-lane ceiling, i.e. "nothing but the JSON lane drops at --max-file-size=1M". That is a fact about this tree's shape, not about the tool, and it expired on 2026-09-09 when docs/EVALS.md reached 1,059,956 B on a landing tip: 1M is 1024*1024 (cli.h parseByteSize), ripwire drops the file as oversize, the header says 16, the arm expects 15, and every shard carrying the gate went red — read as a libstdc++-vs-libc++ split only because the macOS Release leg had been cancelled. main is at 1,042,372 B and this branch at 1,048,058 B; the next append by anyone crosses. Three arms replace it, each an invariant of the tool: the JSON-lane rows at 1M are exactly the BIGJSON files; every other oversize row names a file really over 1,048,576 B on disk (a ceiling never drops a file under it); and skipped_oversize= equals the rows itemized. Verified on three trees: this branch (15 + 0), the tgrep lane merged onto it with EVALS.md at 1,065,642 B (15 + 1, was red), and the tgrep lane tip alone (15 + 0). A tree with no non-JSON file over 1 MiB passes the new arms exactly where the old one did, so nothing is weakened. Co-Authored-By: Claude Fable 5.1 --- test/maxfilesizecheck.sh | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/test/maxfilesizecheck.sh b/test/maxfilesizecheck.sh index 4bea6e9b..80df35b8 100755 --- a/test/maxfilesizecheck.sh +++ b/test/maxfilesizecheck.sh @@ -102,11 +102,38 @@ BIGJSON="$( cd "$ROOT" && git ls-files -z '*.json' | xargs -0 -I{} sh -c 'test $ || no "premise FAILED: no >256K .json in the tree — the invariant arm above cannot discriminate" # and the drop is disclosed at a ceiling that no longer catches it generically (this is the red-first arm: -# the pre-fix binary reports the attribute ABSENT here, so the count reads as a false zero) +# the pre-fix binary reports the attribute ABSENT here, so the count reads as a false zero). +# +# 2026-09-09: the expectation is DERIVED from the tool's own --skipped rows, no longer assumed. The old form +# asserted skipped_oversize == BIGJSON, i.e. "nothing but the JSON lane drops at 1M" — a fact about this +# tree's SHAPE, not about the tool — and it expired the day docs/EVALS.md crossed 1,048,576 B (1M is +# 1024*1024, cli.h parseByteSize): every shard carrying this gate went red on a landing tip, and it read as a +# libstdc++-vs-libc++ split only because the macOS Release leg had been cancelled. Three arms replace it, +# each an invariant of the TOOL: (a) the JSON-lane rows at 1M are exactly the BIGJSON files; (b) every +# OTHER oversize row names a file that really is over 1,048,576 B on disk — a ceiling never drops a file +# under it; (c) the header's skipped_oversize= equals the rows itemized. A tree with no non-JSON file over +# 1 MiB passes (b) and (c) vacuously with OTHERROWS=0, which is the old arm's case, so nothing is weakened. +SKIPPED_ROWS="$( "$BIN" "$ROOT" --max-file-size=1M --skipped 2>/dev/null | tr '>' '\n' | grep 'why="oversize"' )" SKIP1M="$( "$BIN" "$ROOT" --max-file-size=1M --top-k=1 2>/dev/null | grep -oE 'skipped_oversize=[0-9]+' | head -1 | grep -oE '[0-9]+' )" -[ "${SKIP1M:-0}" = "${BIGJSON:-0}" ] \ - && ok "--max-file-size=1M discloses skipped_oversize=$SKIP1M — the $BIGJSON .json the JSON-lane ceiling dropped" \ - || no "--max-file-size=1M reports skipped_oversize=${SKIP1M:-}, expected $BIGJSON (the JSON-lane drop is uncounted)" +JSONROWS="$( printf '%s\n' "$SKIPPED_ROWS" | grep -c 'limit="262144"' | tr -d ' ' )" +ALLROWS="$( printf '%s\n' "$SKIPPED_ROWS" | grep -c 'why="oversize"' | tr -d ' ' )" +OTHERROWS=$(( ALLROWS - JSONROWS )) +[ "${JSONROWS:-0}" = "${BIGJSON:-0}" ] \ + && ok "--max-file-size=1M discloses the $BIGJSON .json the JSON-lane ceiling dropped, one row each" \ + || no "--max-file-size=1M itemizes $JSONROWS JSON-lane rows, expected $BIGJSON (the JSON-lane drop is uncounted)" +UNDER=0 +while IFS= read -r p; do + [ -n "$p" ] || continue + [ "$( wc -c <"$ROOT/$p" | tr -d ' ' )" -gt 1048576 ] || UNDER=$(( UNDER + 1 )) +done <} disagrees with the itemized rows ($JSONROWS + $OTHERROWS)" # ── 3. still valid XML and still deterministic under the ceiling "$BIN" "$ROOT" --max-file-size=8K --top-k=3 >"$TMP/d1" 2>/dev/null From ebfa6b74b7f1fc3f303a503b8d2c4e623a296db6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 12:57:44 -0400 Subject: [PATCH 3/8] =?UTF-8?q?fix(chaconefix):=20unique=20class=20names,?= =?UTF-8?q?=20and=20the=20README=20callers=20rows=20re-captured=20?= =?UTF-8?q?=E2=80=94=20two=20shard=20reds=20from=20the=20first=20CI=20run?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recallevalcheck pins `--for=Robot` over the whole repository to test/chafix/cha.cpp's Robot at rank 1; the new fixture had duplicated chafix's Animal/Dog/Cat/Robot/speak, so name-exact routing had two candidates and four shard-1 jobs went red. The fixture is now Creature/Hound/Lynx/Automaton/Lamp with vocalize(), the same shape under names no other gate pins; chaconecheck, recallevalcheck and manifestcheck pass. readmeexamplecheck pins README's `--callers=rankGraphTeleport` example rows to live paths; the ChaConeMemo insertion moved rankGraph to src/graph.h:3099 and anchoredLexicalRank to :3648, so the two rows are re-captured from the live command (two shard-2 jobs). Co-Authored-By: Claude Fable 5.1 --- README.md | 4 ++-- test/chaconecheck.sh | 37 +++++++++++++++++++------------------ test/chaconefix/a.cpp | 4 ++-- test/chaconefix/b.cpp | 10 +++++----- test/chaconefix/zoo.h | 26 +++++++++++++------------- 5 files changed, 41 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 81b95cf4..f9a10ab3 100644 --- a/README.md +++ b/README.md @@ -1130,8 +1130,8 @@ grow — nothing can keep a line number true in a document, so it is not claimed $ ripwire . --callers=rankGraphTeleport - - + + diff --git a/test/chaconecheck.sh b/test/chaconecheck.sh index 60a10c5e..90c37360 100755 --- a/test/chaconecheck.sh +++ b/test/chaconecheck.sh @@ -1,5 +1,6 @@ #!/usr/bin/env bash -# chaconecheck.sh — gate for the B2.1 CHA-lite cone MEMO (perf round 2026-09-09, the super-linear warm +# chaconecheck.sh — gate for the B2.1 CHA-lite cone MEMO (fixture names are deliberately UNIQUE across test/: +# recallevalcheck pins --for=Robot repo-wide to chafix, so this hierarchy is Creature/Hound/Lynx/Automaton/Lamp) (perf round 2026-09-09, the super-linear warm # --grep floor): graph.h::buildGraph used to recompute a receiver type's inheritance cone — {type} ∪ # ancestors ∪ descendants, two BFS walks over the class-NAME graph with an O(n²) std::find dedup and a # 4096-entry cap per walk — on EVERY still-ambiguous receiver-typed call. Measured on llvm-project @@ -9,9 +10,9 @@ # # WHAT THIS GATE PINS: that the memoised cone is the SAME SET the per-call walk produced — including the # two behaviours a re-implementation is most likely to change silently: -# (a) the cone is keyed on the RECEIVER TYPE, not the callee name: g1/g2 (Dog, two files) and g3 (Cat) -# call the same `speak` and must get their OWN cones; a memo keyed on the callee would hand Cat the -# Dog answer (both contain Animal — so arm 3 pins the *Lamp* case too, where the answers differ); +# (a) the cone is keyed on the RECEIVER TYPE, not the callee name: g1/g2 (Hound, two files) and g3 (Lynx) +# call the same `vocalize` and must get their OWN cones; a memo keyed on the callee would hand Lynx the +# Hound answer (both contain Creature — so arm 3 pins the *Lamp* case too, where the answers differ); # (b) the BFS cap is `out.size() < 4096` checked at the OUTER loop only: the adjacency list that crosses # the cap is pushed whole, and nothing AFTER it is expanded. Arm 5 builds Base→{A,B}, A→A1..A4095, # B→B1: Base's list [A,B] is expanded, A's 4095 children cross the cap, B is never expanded, so B1 is @@ -46,31 +47,31 @@ cd "$ROOT" echo "chaconecheck: BIN=$BIN CORPUS=test/chaconefix (+ a generated 4,097-class cap corpus)" # def lines derived from the source so the gate survives fixture edits -ANIMAL_LINE="$( grep -n 'inline void Animal::speak()' "$FIX/zoo.h" | cut -d: -f1 )" -ROBOT_LINE="$( grep -n 'void speak() { power' "$FIX/zoo.h" | cut -d: -f1 )" +ANIMAL_LINE="$( grep -n 'inline void Creature::vocalize()' "$FIX/zoo.h" | cut -d: -f1 )" +ROBOT_LINE="$( grep -n 'void vocalize() { power' "$FIX/zoo.h" | cut -d: -f1 )" -# distinct speak target lines a caller resolves to (--callees rows carry p="zoo.h:LINE") +# distinct vocalize target lines a caller resolves to (--callees rows carry p="zoo.h:LINE") targets(){ "$BIN" "$FIX" --callees="$1" --no-cache 2>/dev/null | tr '>' '\n' | grep -oE 'zoo\.h:[0-9]+' | grep -oE '[0-9]+$' | sort -un; } count(){ "$BIN" "$FIX" --callees="$1" --no-cache 2>/dev/null | grep -oE ' count="[0-9]+"' | head -1 | grep -oE '[0-9]+'; } hasamb(){ "$BIN" "$FIX" --no-cache --top-k=100000 2>/dev/null | grep -oE "n=\"$1\"[^>]*" | grep -q 'amb='; } -# ── 1) the first Dog cone (a.cpp): Animal::speak ONLY, Robot dropped, no amb ──────────────────────────── +# ── 1) the first Hound cone (a.cpp): Creature::vocalize ONLY, Automaton dropped, no amb ──────────────────────────── T="$( targets g1 )" if [ "$( count g1 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && ! hasamb g1; then - ok "g1 (Dog, a.cpp): Animal::speak only (zoo.h:$ANIMAL_LINE), Robot dropped, no amb=" -else no "g1 (Dog, a.cpp): expected exactly Animal::speak — got count=$( count g1 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + ok "g1 (Hound, a.cpp): Creature::vocalize only (zoo.h:$ANIMAL_LINE), Automaton dropped, no amb=" +else no "g1 (Hound, a.cpp): expected exactly Creature::vocalize — got count=$( count g1 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi -# ── 2) the memo HIT (b.cpp asks for the Dog cone again): byte-identical answer ────────────────────────── +# ── 2) the memo HIT (b.cpp asks for the Hound cone again): byte-identical answer ────────────────────────── T="$( targets g2 )" if [ "$( count g2 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && ! hasamb g2; then - ok "g2 (Dog, b.cpp — memo hit): Animal::speak only, Robot dropped, no amb=" -else no "g2 (Dog, b.cpp): memo hit differs from g1 — got count=$( count g2 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + ok "g2 (Hound, b.cpp — memo hit): Creature::vocalize only, Automaton dropped, no amb=" +else no "g2 (Hound, b.cpp): memo hit differs from g1 — got count=$( count g2 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi -# ── 3) a DIFFERENT cone on the same callee name (Cat), and one with NO inheritance facts (Lamp) ───────── +# ── 3) a DIFFERENT cone on the same callee name (Lynx), and one with NO inheritance facts (Lamp) ───────── T="$( targets g3 )" if [ "$( count g3 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! hasamb g3; then - ok "g3 (Cat): its own cone {Cat, Animal} → Animal::speak only, no amb=" -else no "g3 (Cat): expected exactly Animal::speak — got count=$( count g3 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + ok "g3 (Lynx): its own cone {Lynx, Creature} → Creature::vocalize only, no amb=" +else no "g3 (Lynx): expected exactly Creature::vocalize — got count=$( count g3 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi T="$( targets g4 )" if [ "$( count g4 )" = 2 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && hasamb g4; then ok "g4 (Lamp): cone {Lamp} keeps nothing → DEGRADE, both targets kept, amb= honest" @@ -79,8 +80,8 @@ else no "g4 (Lamp): expected the untouched 2-way split — got count=$( count g4 # ── 4) control: a parameter receiver has no var→type binding, so no cone can fire ─────────────────────── T="$( targets g5 )" if [ "$( count g5 )" = 2 ] && hasamb g5; then - ok "g5 (Dog& parameter): receiver type unknown → 2-way split kept, amb= honest (control)" -else no "g5 (Dog& parameter): control should stay ambiguous — got count=$( count g5 )"; fi + ok "g5 (Hound& parameter): receiver type unknown → 2-way split kept, amb= honest (control)" +else no "g5 (Hound& parameter): control should stay ambiguous — got count=$( count g5 )"; fi # ── 5) the 4096 BFS cap, reproduced exactly: Base→{A,B}, A→A1..A4095, B→B1; B is never expanded ───────── CAP="$TMP/capfix"; mkdir -p "$CAP" diff --git a/test/chaconefix/a.cpp b/test/chaconefix/a.cpp index debf69bb..3cc2c0c9 100644 --- a/test/chaconefix/a.cpp +++ b/test/chaconefix/a.cpp @@ -1,4 +1,4 @@ -// chaconefix/a.cpp — the FIRST call whose receiver static type is Dog: the Dog cone is computed here. +// chaconefix/a.cpp — the FIRST call whose receiver static type is Hound: the Hound cone is computed here. #include "zoo.h" -void g1() { Dog d; d.speak(); } // cone {Dog, Animal} → Animal::speak only; Robot::speak dropped +void g1() { Hound d; d.vocalize(); } // cone {Hound, Creature} → Creature::vocalize only; Automaton::vocalize dropped diff --git a/test/chaconefix/b.cpp b/test/chaconefix/b.cpp index 1c0d27bb..3df7f97e 100644 --- a/test/chaconefix/b.cpp +++ b/test/chaconefix/b.cpp @@ -1,8 +1,8 @@ -// chaconefix/b.cpp — the SECOND file to ask for the Dog cone (a memo HIT must equal the first answer), +// chaconefix/b.cpp — the SECOND file to ask for the Hound cone (a memo HIT must equal the first answer), // plus a different cone on the same callee name, a receiver with no inheritance facts, and a control. #include "zoo.h" -void g2() { Dog d; d.speak(); } // memo hit: byte-identical to g1's answer (Animal::speak only) -void g3() { Cat c; c.speak(); } // a DIFFERENT cone {Cat, Animal} keyed on the same callee `speak` -void g4() { Lamp l; l.speak(); } // cone {Lamp} keeps nothing → DEGRADE: tier untouched, stays ambiguous -void g5( Dog& p ) { p.speak(); } // control: parameter receiver → no var→type binding → cone cannot fire +void g2() { Hound d; d.vocalize(); } // memo hit: byte-identical to g1's answer (Creature::vocalize only) +void g3() { Lynx c; c.vocalize(); } // a DIFFERENT cone {Lynx, Creature} keyed on the same callee `vocalize` +void g4() { Lamp l; l.vocalize(); } // cone {Lamp} keeps nothing → DEGRADE: tier untouched, stays ambiguous +void g5( Hound& p ) { p.vocalize(); } // control: parameter receiver → no var→type binding → cone cannot fire diff --git a/test/chaconefix/zoo.h b/test/chaconefix/zoo.h index 696605f0..43905bc5 100644 --- a/test/chaconefix/zoo.h +++ b/test/chaconefix/zoo.h @@ -1,36 +1,36 @@ // chaconefix/zoo.h — gate fixture for the CHA-lite cone memo (test/chaconecheck.sh). // -// The same hierarchy shape as chafix/cha.cpp, kept in ONE header so every candidate for `speak` sits in +// The same hierarchy shape as chafix/cha.cpp under UNIQUE names (recallevalcheck pins --for=Robot to chafix), in ONE header so every candidate for `vocalize` sits in // the SAME directory as every caller (tier 2 of the ladder) and the tier reaches CHA-lite still ambiguous: -// Animal — a base with a bodied speak(); Dog and Cat implement it WITHOUT overriding speak(); -// Robot — UNRELATED, its own speak(); Lamp — a class with NO inheritance facts and NO speak(). -struct Animal +// Creature — a base with a bodied vocalize(); Hound and Lynx implement it WITHOUT overriding vocalize(); +// Automaton — UNRELATED, its own vocalize(); Lamp — a class with NO inheritance facts and NO vocalize(). +struct Creature { - void speak(); // declared here, defined out-of-line below (a real, bodied def) - virtual void move(); // an unrelated virtual so Animal is a genuine polymorphic base + void vocalize(); // declared here, defined out-of-line below (a real, bodied def) + virtual void move(); // an unrelated virtual so Creature is a genuine polymorphic base int tag = 0; }; -struct Dog : Animal // implementor 1 — does NOT define speak (inherits Animal::speak) +struct Hound : Creature // implementor 1 — does NOT define vocalize (inherits Creature::vocalize) { void move() override { tag = 1; } }; -struct Cat : Animal // implementor 2 — a DIFFERENT cone on the same callee name +struct Lynx : Creature // implementor 2 — a DIFFERENT cone on the same callee name { void move() override { tag = 2; } }; -struct Robot // UNRELATED — outside every Animal cone +struct Automaton // UNRELATED — outside every Creature cone { - void speak() { power = 1; } + void vocalize() { power = 1; } int power = 0; }; -struct Lamp // no bases, no derived classes, no speak(): its cone is {Lamp} alone +struct Lamp // no bases, no derived classes, no vocalize(): its cone is {Lamp} alone { int watts = 0; }; -inline void Animal::speak() { tag = 3; } -inline void Animal::move() { tag = 4; } +inline void Creature::vocalize() { tag = 3; } +inline void Creature::move() { tag = 4; } From a90d206fc9b946c993215a2b6651f25cd3212168 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 13:04:55 -0400 Subject: [PATCH 4/8] =?UTF-8?q?test(chaconecheck):=20a=20memo=20hit=20afte?= =?UTF-8?q?r=20the=20memo=20has=20grown=20is=20the=20requester's=20own=20c?= =?UTF-8?q?one=20=E2=80=94=20arm=206,=20mutation-proven?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The memo cannot go stale in the invalidation sense: it lives inside one buildGraph call and its inputs (chaUp/chaDown) are immutable for that lifetime. The failure this class of change actually has is a hit that returns the WRONG hierarchy — a cached cone pointer, an index that goes wrong once the cone vector reallocates, a key collision. Arm 6 discriminates it: a third hierarchy (Machine, Droid : Machine) whose cone excludes Creature is filled AFTER Hound's entry exists, then Hound is asked for again; the answer must be Creature::vocalize and never Machine::vocalize. Mutating coneFor to return the newest cone on a hit (`&cones_.back()`) turns exactly that arm red (observed: count=1, the Machine line); the revert is green. The Lamp and parameter-receiver arms now see a 3-way split, which is the same DEGRADE and control under the larger fixture. docs/EVALS.md gains the correctness finding the per-line regex timing surfaced — `^#include` on llvm-project: 1,487 hits on main, 289,646 on the tgrep lane's line-anchored scan, and the per-line shape faster because it bounds `.*` to a line — stated with attribution to that lane's fix. The file is now 1,049,181 B, past the 1M ceiling; test/maxfilesizecheck.sh's derived arm passes on this tip with 15 JSON-lane + 1 ceiling rows, so the fix in the previous commit is exercised by the real tree, not only by a merged copy. Co-Authored-By: Claude Fable 5.1 --- docs/EVALS.md | 14 +++++++++++++- test/chaconecheck.sh | 32 +++++++++++++++++++++++++------- test/chaconefix/b.cpp | 2 ++ test/chaconefix/zoo.h | 12 ++++++++++++ 4 files changed, 52 insertions(+), 8 deletions(-) diff --git a/docs/EVALS.md b/docs/EVALS.md index 6fa41e24..675a876f 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -13076,7 +13076,19 @@ llvm rung, the same argv (`--grep-in=any`), the plain build with the memo, warm, | R1 `^#include` | 233.06 s | 10.08 s — not comparable: this branch predates the lane's line-anchor fix | Every query is now within a second of the absent literal: the scan is still hidden behind the graph, the -graph is just 17× smaller. Re-deriving the lane's Q\* formula with its own tgrep numbers (B = 11.113 s, +graph is just 17× smaller. + +**A correctness finding the timing table surfaced, stated as one.** The tgrep lane replaces `grepScanText`'s +one-iterator-per-file regex scan with one per LINE, so that `^` and `$` mean line anchors. Priced interleaved on +the scan's own `grep/1` scope with the lane merged onto this fix (host load 31; the pair-wise scope, not the +wall clock, is the comparison): on llvm-project R1 `^#include` returns **1,487 hits on main and 289,646 on +the lane** — 288,159 matches today's shipped binary misses silently, on one query, while reporting a confident +count — and the per-line shape is *faster* (3.40 s against 4.48 s), because bounding each search to a line +stops `.*` from running across lines, so it does strictly less work per attempt. R3 `malloc.*free` 2.28 → 1.68 s; +the prefilter-defeating R4, which scans every byte, 1.27 → 1.50 s (+0.22 s on 2.9 GB, ≈3 ns a line); the literal +control 1.16 → 1.20 s (an unchanged path: the noise floor). On go every pair is within noise or faster, +including a forced full scan. Nothing super-linear, and the largest cost is 18% on the one pattern with no +literal at all. Re-deriving the lane's Q\* formula with its own tgrep numbers (B = 11.113 s, q_index = 2.7513 s over the same six) and a post-fix q_scan of ≈ 9.3 s gives **Q\* ≈ 1.7 queries against ripwire-warm** where the lane read 0.1 — the resident index still pays for itself inside a two-query session at this scale, but no longer "before the first query finishes". tgrep itself was not re-run; only diff --git a/test/chaconecheck.sh b/test/chaconecheck.sh index 90c37360..a8d7da0a 100755 --- a/test/chaconecheck.sh +++ b/test/chaconecheck.sh @@ -19,12 +19,19 @@ # OUTSIDE the cone and B1::m is dropped while all 4095 A-children survive → count=4095, and the ONE # call site is a 4095-way split → amb="1" (amb counts ambiguous CALLS, not tier width). # A cap moved to the inner loop, or to 4097, or a dedup that changes discovery order, turns it red. +# (c) a memo HIT served AFTER the memo has grown is the requester's own cone, not the newest one: g7 asks for +# Hound again after g6 filled Droid's cone {Droid, Machine}; Hound's answer must still be Creature::vocalize +# and never Machine::vocalize. Staleness in the other sense is impossible by construction — the memo lives +# inside ONE buildGraph call and its inputs (chaUp/chaDown) are immutable for that lifetime, so there is +# nothing to invalidate — but a cached cone pointer, an index that goes wrong once the cone vector +# reallocates, or a key collision all present exactly as arm 6 reads: the wrong hierarchy's answer on a hit. # Plus the degrade rule (a cone that keeps nothing leaves the tier untouched — g4), a control where the cone # cannot fire (g5), determinism and XML well-formedness. # # RED-first proof (2026-09-09): against the pre-memo binary every arm PASSES (the expected values are the # per-call walk's own answers, hand-derived above); mutating the cap to `< 4097` fails arm 5; keying the memo -# on the callee name fails arm 3. +# on the callee name fails arm 3; returning the most recently filled cone on a hit (`&cones_.back()`) fails +# arm 6 — observed red, then green after the revert, on 2026-09-09. # # test/chaconecheck.sh # uses build/ripwire # RIPWIRE_BIN=asan/ripwire test/chaconecheck.sh @@ -47,7 +54,8 @@ cd "$ROOT" echo "chaconecheck: BIN=$BIN CORPUS=test/chaconefix (+ a generated 4,097-class cap corpus)" # def lines derived from the source so the gate survives fixture edits -ANIMAL_LINE="$( grep -n 'inline void Creature::vocalize()' "$FIX/zoo.h" | cut -d: -f1 )" +ANIMAL_LINE="$( grep -n 'inline void Creature::vocalize()' "$FIX/zoo.h" | cut -d: -f1 )" +MACHINE_LINE="$( grep -n 'inline void Machine::vocalize()' "$FIX/zoo.h" | cut -d: -f1 )" ROBOT_LINE="$( grep -n 'void vocalize() { power' "$FIX/zoo.h" | cut -d: -f1 )" # distinct vocalize target lines a caller resolves to (--callees rows carry p="zoo.h:LINE") @@ -73,16 +81,26 @@ if [ "$( count g3 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! ok "g3 (Lynx): its own cone {Lynx, Creature} → Creature::vocalize only, no amb=" else no "g3 (Lynx): expected exactly Creature::vocalize — got count=$( count g3 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi T="$( targets g4 )" -if [ "$( count g4 )" = 2 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && hasamb g4; then - ok "g4 (Lamp): cone {Lamp} keeps nothing → DEGRADE, both targets kept, amb= honest" -else no "g4 (Lamp): expected the untouched 2-way split — got count=$( count g4 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi +if [ "$( count g4 )" = 3 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && printf '%s\n' "$T" | grep -qx "$ROBOT_LINE" && printf '%s\n' "$T" | grep -qx "$MACHINE_LINE" && hasamb g4; then + ok "g4 (Lamp): cone {Lamp} keeps nothing → DEGRADE, all three targets kept, amb= honest" +else no "g4 (Lamp): expected the untouched 3-way split — got count=$( count g4 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi # ── 4) control: a parameter receiver has no var→type binding, so no cone can fire ─────────────────────── T="$( targets g5 )" -if [ "$( count g5 )" = 2 ] && hasamb g5; then - ok "g5 (Hound& parameter): receiver type unknown → 2-way split kept, amb= honest (control)" +if [ "$( count g5 )" = 3 ] && hasamb g5; then + ok "g5 (Hound& parameter): receiver type unknown → 3-way split kept, amb= honest (control)" else no "g5 (Hound& parameter): control should stay ambiguous — got count=$( count g5 )"; fi +# ── 6) a hit AFTER the memo grew: g6 fills Droid's cone, then g7 asks for Hound again ────────────────────── +T="$( targets g6 )" +if [ "$( count g6 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$MACHINE_LINE" && ! hasamb g6; then + ok "g6 (Droid): third cone {Droid, Machine} → Machine::vocalize only (zoo.h:$MACHINE_LINE), no amb=" +else no "g6 (Droid): expected exactly Machine::vocalize — got count=$( count g6 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi +T="$( targets g7 )" +if [ "$( count g7 )" = 1 ] && printf '%s\n' "$T" | grep -qx "$ANIMAL_LINE" && ! printf '%s\n' "$T" | grep -qx "$MACHINE_LINE" && ! hasamb g7; then + ok "g7 (Hound, after the memo grew): its OWN cone again — Creature::vocalize, never Machine::vocalize" +else no "g7 (Hound, after the memo grew): a hit returned the wrong hierarchy — got count=$( count g7 ) lines={$( printf '%s' "$T" | tr '\n' ' ')}"; fi + # ── 5) the 4096 BFS cap, reproduced exactly: Base→{A,B}, A→A1..A4095, B→B1; B is never expanded ───────── CAP="$TMP/capfix"; mkdir -p "$CAP" { diff --git a/test/chaconefix/b.cpp b/test/chaconefix/b.cpp index 3df7f97e..382af7ea 100644 --- a/test/chaconefix/b.cpp +++ b/test/chaconefix/b.cpp @@ -6,3 +6,5 @@ void g2() { Hound d; d.vocalize(); } // memo hit: byte-identical to g1's void g3() { Lynx c; c.vocalize(); } // a DIFFERENT cone {Lynx, Creature} keyed on the same callee `vocalize` void g4() { Lamp l; l.vocalize(); } // cone {Lamp} keeps nothing → DEGRADE: tier untouched, stays ambiguous void g5( Hound& p ) { p.vocalize(); } // control: parameter receiver → no var→type binding → cone cannot fire +void g6() { Droid d; d.vocalize(); } // a THIRD cone {Droid, Machine}: the memo grows AFTER Hound's entry exists +void g7() { Hound h; h.vocalize(); } // Hound AGAIN, after the memo grew: a hit must be Hound's cone, not the newest diff --git a/test/chaconefix/zoo.h b/test/chaconefix/zoo.h index 43905bc5..d27e5739 100644 --- a/test/chaconefix/zoo.h +++ b/test/chaconefix/zoo.h @@ -32,5 +32,17 @@ struct Lamp // no bases, no derived classes, no vocalize(): int watts = 0; }; +struct Machine // a SECOND base with its own bodied vocalize(): the Droid cone excludes Creature +{ + void vocalize(); + int rpm = 0; +}; + +struct Droid : Machine // implementor of Machine — does NOT define vocalize (inherits Machine::vocalize) +{ + int model = 0; +}; + inline void Creature::vocalize() { tag = 3; } +inline void Machine::vocalize() { rpm = 5; } inline void Creature::move() { tag = 4; } From f3fb49bee86f122a5051ada24219a898ed379645 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 13:08:05 -0400 Subject: [PATCH 5/8] fix(graph): ChaConeMemo hands out an index, never a pointer into a growing table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cone held `const std::vector*` into cones_, a vector that grows on every first-time fill, so the handle was valid only until the next coneFor() — an invariant stated in a comment and enforced by nothing. The one call site used it immediately, so it was safe today; the next call site, or a hoist out of the loop, would have been a use-after-free in graph construction that ASan catches only when the reallocation happens to move the buffer. The handle is now the cone's index, resolved inside contains() on every membership test, so no handle can dangle for the memo's lifetime. Output unchanged: default map on go byte-identical to the previous build; chaconecheck, chacheck, callerscheck, nsfiltercheck, localitycheck green. README's --callers example rows re-captured for the two-line shift (the same line-number pin that fired on the first CI run; a documentation gate pinned to source line numbers is its own fragility, noted in the round's report). Co-Authored-By: Claude Fable 5.1 --- src/graph.h | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/graph.h b/src/graph.h index c3127253..00f20a39 100644 --- a/src/graph.h +++ b/src/graph.h @@ -566,11 +566,12 @@ struct ChaConeMemo static constexpr std::size_t kChaConeCap = 4096; // per-walk discovery cap, unchanged from the per-call walk static constexpr std::uint32_t kNoCone = 0xFFFFFFFFu; - // A cone handle: `ids == nullptr` ⇒ the receiver type has no inheritance facts (membership is equality with - // the type itself). Valid until the next coneFor() call — cones live in a growing vector. + // A cone handle: an INDEX into the memo's cone table, resolved inside contains() on every use, so no handle + // can dangle when the table grows (a raw pointer would be invalidated by the next fill). `index == kNoCone` + // ⇒ the receiver type has no inheritance facts: membership is equality with the type itself. struct Cone { - const std::vector* ids; + std::uint32_t index; }; ChaConeMemo( const HashMap>& chaUp, @@ -607,7 +608,7 @@ struct ChaConeMemo const auto rit = idOf_.find( key_ ); if( rit == idOf_.end() ) { - return Cone{ nullptr }; + return Cone{ kNoCone }; } const std::uint32_t root = rit->second; if( coneIndex_[ root ] == kNoCone ) @@ -621,19 +622,20 @@ struct ChaConeMemo coneIndex_[ root ] = std::uint32_t( cones_.size() ); cones_.push_back( std::move( cone ) ); } - return Cone{ &cones_[ coneIndex_[ root ] ] }; + return Cone{ coneIndex_[ root ] }; } // Is a candidate's enclosing scope inside `cone`? A scope no inheritance fact ever named cannot be in any // interned cone; a scope-less free function is never a member-call target and is correctly excluded. bool contains( Cone cone, std::string_view recvType, const std::string& scope ) const { - if( cone.ids == nullptr ) + if( cone.index == kNoCone ) { return scope == recvType; } - const auto it = idOf_.find( scope ); - return it != idOf_.end() && std::binary_search( cone.ids->begin(), cone.ids->end(), it->second ); + const auto it = idOf_.find( scope ); + const std::vector& ids = cones_[ cone.index ]; // resolved NOW, never held across a fill + return it != idOf_.end() && std::binary_search( ids.begin(), ids.end(), it->second ); } private: From 7e85e793ae5e3a00454fb3f5d03bb130e7a581b3 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 13:08:46 -0400 Subject: [PATCH 6/8] fix(readme): the --callers example rows, re-captured for the index-handle shift (the previous commit's edit did not apply) The re-capture in 8bea4490 split the live rows on whitespace and matched nothing, so README kept the old line numbers and readmeexamplecheck stayed red; this is the same two-row re-capture, done from the live command and verified green before the commit. Co-Authored-By: Claude Fable 5.1 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f9a10ab3..548e4fe9 100644 --- a/README.md +++ b/README.md @@ -1130,8 +1130,8 @@ grow — nothing can keep a line number true in a document, so it is not claimed $ ripwire . --callers=rankGraphTeleport - - + + From e6f5df3acd89b10a76407dc8a2eaa17a64b9c5ae Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 14:01:37 -0400 Subject: [PATCH 7/8] =?UTF-8?q?ci:=20drop=20the=20runner=20image's=20Chrom?= =?UTF-8?q?e=20apt=20source=20before=20any=20apt-get=20update=20=E2=80=94?= =?UTF-8?q?=20a=20Hash=20Sum=20mismatch=20there=20killed=20every=20Linux?= =?UTF-8?q?=20job?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit At 17:51–17:56 UTC on 2026-09-09 every ubuntu-24.04 job on this repository — main's own run 34385457240, three lanes' runs, 18 jobs each — died in "Install tooling (Linux)" before compiling anything: Err:29 https://dl.google.com/linux/chrome-stable/deb stable/main amd64 Packages E: Failed to fetch .../binary-amd64/Packages.gz Hash Sum mismatch ##[error]Process completed with exit code 100. The ubuntu-24.04 runner image ships /etc/apt/sources.list.d/google-chrome.list; nothing here installs from it, but `apt-get update` exits 100 when any configured index fails, and apt.llvm.org's llvm.sh runs its own `apt-get update`, so the style job dies one line later at `clang-format --version`. Removing that source as the first line of each Linux install step (four sites: style, the release matrix, the fallback emitter, asan) makes the workflow independent of a mirror it never uses. No gate, no build flag, no version changes. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f44b89e..4b92db9a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,6 +87,7 @@ jobs: - name: Install clang-format / clang-tidy (PINNED major — see the job comment) run: | + sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh chmod +x /tmp/llvm.sh sudo /tmp/llvm.sh "$CLANG_VERSION" all @@ -190,6 +191,7 @@ jobs: - name: Install tooling (Linux) if: runner.os == 'Linux' run: | + sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc clang @@ -268,6 +270,7 @@ jobs: fetch-depth: 0 - name: Install tooling run: | + sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc - name: Configure with the image's stock gcc 13 (plain flavour) @@ -419,6 +422,7 @@ jobs: - name: Install tooling (Linux) if: runner.os == 'Linux' run: | + sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc clang From b7a1767fb81c94f845862eaac643b313b83991a5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I Date: Wed, 9 Sep 2026 14:05:39 -0400 Subject: [PATCH 8/8] =?UTF-8?q?ci:=20the=20Chrome=20apt=20source=20on=20ub?= =?UTF-8?q?untu-24.04=20is=20a=20deb822=20.sources=20file=20=E2=80=94=20ma?= =?UTF-8?q?tch=20google-chrome*=20and=20list=20the=20directory?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous line removed google-chrome*.list and the Chrome index was still fetched on run 34386505159: the 24.04 image carries /etc/apt/sources.list.d/google-chrome.sources. Six of eight sampled jobs cleared the install step only because the mirror happened to answer consistently; five did not. The glob now matches both spellings, and the step prints the directory first so the next miss names itself in the log. Co-Authored-By: Claude Fable 5.1 --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b92db9a..538e5808 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: Install clang-format / clang-tidy (PINNED major — see the job comment) run: | - sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 + ls /etc/apt/sources.list.d/ && sudo rm -f /etc/apt/sources.list.d/google-chrome* # the runner image ships a Chrome apt source (deb822 .sources on 24.04, not .list); its "Hash Sum mismatch" killed every Linux job on 2026-09-09 wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh chmod +x /tmp/llvm.sh sudo /tmp/llvm.sh "$CLANG_VERSION" all @@ -191,7 +191,7 @@ jobs: - name: Install tooling (Linux) if: runner.os == 'Linux' run: | - sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 + ls /etc/apt/sources.list.d/ && sudo rm -f /etc/apt/sources.list.d/google-chrome* # the runner image ships a Chrome apt source (deb822 .sources on 24.04, not .list); its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc clang @@ -270,7 +270,7 @@ jobs: fetch-depth: 0 - name: Install tooling run: | - sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 + ls /etc/apt/sources.list.d/ && sudo rm -f /etc/apt/sources.list.d/google-chrome* # the runner image ships a Chrome apt source (deb822 .sources on 24.04, not .list); its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc - name: Configure with the image's stock gcc 13 (plain flavour) @@ -422,7 +422,7 @@ jobs: - name: Install tooling (Linux) if: runner.os == 'Linux' run: | - sudo rm -f /etc/apt/sources.list.d/google-chrome*.list # the runner image ships a Chrome apt source; its "Hash Sum mismatch" killed every Linux job on 2026-09-09 + ls /etc/apt/sources.list.d/ && sudo rm -f /etc/apt/sources.list.d/google-chrome* # the runner image ships a Chrome apt source (deb822 .sources on 24.04, not .list); its "Hash Sum mismatch" killed every Linux job on 2026-09-09 sudo apt-get update sudo apt-get install -y --no-install-recommends libxml2-utils ripgrep bc clang