Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
7ece90b
fix(lang): five code extensions the index parses were no language to …
joyful-ii-V-I Sep 16, 2026
3089304
build(warnings): a switch that forgot an enumerator compiled on every…
joyful-ii-V-I Sep 16, 2026
97220a6
fix(cache): cache and layout facts only a shell gate or a comment hel…
joyful-ii-V-I Sep 16, 2026
084ab17
fix(emit): a memory buffer that lost a write was read back as a whole…
joyful-ii-V-I Sep 16, 2026
22001df
fix(redact): the first-byte rule mask numbers kRedactRules by hand, a…
joyful-ii-V-I Sep 16, 2026
0a2a398
fix(masks): a count shifted into a fixed-width mask at runtime had no…
joyful-ii-V-I Sep 16, 2026
6eee868
fix(tables): a table indexed by an enum could be one row short and co…
joyful-ii-V-I Sep 16, 2026
d41c7e4
Merge origin/main (31e788ce) into lane/compile-time-checks
joyful-ii-V-I Sep 16, 2026
fac0e67
chore(quality): ack the thirteen findings this lane's compile-time fe…
joyful-ii-V-I Sep 16, 2026
109e7eb
Merge origin/main (105666c1) into lane/compile-time-checks
joyful-ii-V-I Sep 17, 2026
d5af70e
Merge origin/main (f7bbb04a) into lane/compile-time-checks
joyful-ii-V-I Sep 17, 2026
597f0e4
test(crashsweep): S2's memstream rows named twelve sites that no long…
joyful-ii-V-I Sep 17, 2026
4fd59d4
fix(nonlocal): a .pyi stub beside its .py counted the module's global…
joyful-ii-V-I Sep 17, 2026
673df2b
fix(trace): --from-trace withholds a bundle whose blocks lost their b…
joyful-ii-V-I Sep 17, 2026
f69ac0f
test(gates): controls that blame only their own mutation, and a --fro…
joyful-ii-V-I Sep 17, 2026
605ac9b
Merge origin/main (f57a9df3) into lane/compile-time-checks
joyful-ii-V-I Sep 17, 2026
d7922a5
Merge origin/main (bcd3b016) into lane/compile-time-checks
joyful-ii-V-I Sep 17, 2026
c1ce76c
fix(emit): MemoryStream's opener overload is openWith, not a second open
joyful-ii-V-I Sep 17, 2026
843582f
fix(nonlocal): the .pyi stub lookup sorts and searches string_views w…
joyful-ii-V-I Sep 17, 2026
491ac9b
test(htmlrender): (N1) derives the language roster from a constexpr l…
joyful-ii-V-I Sep 17, 2026
b8e86b9
docs(capture): re-splice the three graph.h-seeded sections for this t…
joyful-ii-V-I Sep 17, 2026
41d2388
fix(graph): namespaceCompatible's switch is line-neutral, so the rele…
joyful-ii-V-I Sep 17, 2026
3e7d815
Merge origin/main (ea03af88) into lane/compile-time-checks
joyful-ii-V-I Sep 17, 2026
28ea208
fix(mcp): mcpverbs.h includes <optional> for the answer seam it declares
joyful-ii-V-I Sep 17, 2026
eec944c
fix(mcp): a uses answer whose buffer failed is an internal error, not…
joyful-ii-V-I Sep 17, 2026
7f2ee21
fix(for): a degraded pre-render no longer counts its redacted secrets…
joyful-ii-V-I Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/pargates-shard-weights.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
"emptyvaluerefusecheck.sh": 12.1,
"ensembleavailcheck.sh": 6.3,
"ensemblecheck.sh": 2.8,
"enumtablecheck.sh": 4.0,
"essentialcxcheck.sh": 9.9,
"estchargecheck.sh": 131.6,
"evalcheck.sh": 2.2,
Expand Down
13 changes: 13 additions & 0 deletions .ripwire_quality_acks

Large diffs are not rendered by default.

112 changes: 112 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,118 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli

## [Unreleased]

### Changed — the compiler checks the tables, switches, masks and layouts this tree's defects came from

Each check below is written against a defect this repository shipped or nearly shipped, and each was shown failing on a
deliberate break before it landed. None of them changes output. They are `static_assert`s, one template constraint, one
link-time stamp and two warning flags. Output was compared with the base binary on every fixture corpus, stdout and exit
code, and the only differences are the extension fix below.

- **Language registration.** Appending a `Lang` meant updating five tables in four files. 02f798e3 (Dart), 9418e35e (five
unanalysed languages) and PR #233's `.gd` row each stayed one language short. `src/main.cpp` now asserts that every
code language is analysed or disclosed as unanalysed by `--nonlocal-state`, and that it is named by the lint vocabulary,
the lint catalog and `langOfPath`. `src/ingest_crawl.h` asserts that `langOfPath`'s extensions and the crawl's are the
same. Each check returns the first INDEX that is wrong, so a zero-filled row cannot pass. `isCodeLang` (`src/model.h`)
is the one declared exemption, and it has no `default:`.
- **`-Werror=switch -Werror=implicit-fallthrough`** on ripwire's own C++ targets, for every compiler. GCC ran no
`-Wswitch` at all before this, because it enables it only under `-Wall`. The warning count was measured at 0 on
AppleClang 21 and Homebrew clang 22, debug and `-DNDEBUG`, for both binaries and the four test harnesses. A switch that
returns one answer per enumerator carries no `default:` any more. Eleven did, including `dependencyCapable` and
`dependencyDialect`, where Dart was the one language never decided.
- **Cache and layout facts.** `quality.h`'s mirror of `kParserVer` and `kCacheVersion` is asserted equal to the real
constants; only `test/qextractionkeycheck.sh` held that before. `CacheEntry` must have unique object representations,
because `sizeof == 32` did not prove "no padding". `qsnapPut` is constrained the same way, so a padded struct or a float
cannot reach a byte-stable blob. `ingest()` carries `sizeof( Symbol )` and `sizeof( IngestResult )` in its mangled name.
CLAUDE.md records three mixed-layout builds that linked "successfully". Measured on this tree, an object pair compiled
against two `Symbol` layouts now fails to link, where the same pair without the stamp linked and died with SIGBUS.
- **The redaction first-byte mask** is compared bit for bit with the rule table it hand-numbers, so an inserted rule
cannot leave a later rule tried only at bytes its pattern cannot start with.
- **Shift width against count.** Every mask a runtime value is shifted into has its count pinned to its width: the
language masks, the ensemble and quality-panel family masks, the naming-rule mask, the redaction rule mask, the
pack-task subset enumeration and `strkern`'s block masks.
- **Tables indexed by an enum.** A table's extent is deduced and asserted against the enum's count, and the count is
proven exact beside the enum with `infra/enumcount.h` (#241). A spelled extent had let several of these asserts restate
their own declaration, and let a missing row compile as a null pointer. `kNodeFieldNames` rows now name their
enumerator, because the enum and the table are paired by index. `skilleval`'s provenance counters were `[3]` for a
four-value `Prov`. A new gate, `test/enumtablecheck.sh`, refuses a literal-extent table indexed by an enum. It reports 14
subscripts over five tables on `f8e6087c`. Each of its three positive controls puts one real literal back and must
report exactly that table among the violations the literal adds, so a violation already in the tree fails the rule
arm alone instead of every control.

### Fixed — a memory buffer that lost a write was read back as a whole document

Twenty-three places render into an `open_memstream` buffer and then read it back: the map's own children (XML and JSON),
the `est_tokens` payload charges, the `--max-tokens` fit probes, the `--token-budget` buffer, the `--for` lens's pre-rendered
blocks, the `--from-trace` blocks and seven MCP answers. Twenty-two of them flushed and closed the buffer without looking
at either result. The one that did look, `renderToString`, could not see the failure it looked for.

Measured, not assumed: a `DYLD_INSERT_LIBRARIES` interposer failed one chosen `realloc` inside an `open_memstream` on macOS
26.5.1 (Apple libc), over 5 KB, 50 KB and 200 KB streams written in 1 KB chunks. In all 19 runs where the failure landed
inside the stream, one `fwrite` came back short and the stream's error flag was set. Each run lost 152 to 976 bytes, as
late as chunk 177 of 200, so the hole sat in the middle of the document. `fflush` and `fclose` both returned 0 every time.
Read after that, the buffer is a shorter document with no sign that it is one. What that meant per site: a map or `--json`
map with a hole in it; a payload section, trace block or MCP answer cut mid-element; a `--max-tokens` probe that read a
too-small size as fitting; and a `--token-budget` map printed short at exit 0.

Every buffer is now owned by one type, `rw::MemoryStream` (`src/infra/emit.h`). Its `finish()` flushes, reads the error
flag, closes, and reports by value, and it is `[[nodiscard]]`. The destructor closes a stream nobody finished and frees the
buffer on every path, so no site frees or closes anything by hand. A buffer that did not finish whole takes the path a
failed open already took. The map and the JSON map are rendered again, straight to the output, with the modelled
`est_tokens`: the children became one renderer both paths call. A charged section streams uncharged, and a probe answers
"unmeasured". The `--for` blocks are emitted directly, and a secret redacted in the failed buffer is not counted again when the block
re-renders. The MCP answers answer as they do when the open fails, except `uses`, which answers `-32603` instead of an
empty success. Two surfaces have
no second path, because the buffer holds the answer itself, and both refuse in every build instead of printing short.
The `--token-budget` map prints nothing, says `write error — the --token-budget buffer lost bytes` on stderr, and exits
1. `--from-trace` and `--run-trace` do the same when the `<trace>` map, the test hop or the signature/body section loses
its buffer, at the open or at the finish, and the MCP `from_trace` verb answers `-32603`. Those blocks used to be left
out of a bundle printed at exit 0, which no Release build disclosed.

`test/estchargecheck.sh` gains two arms. **#14f** uses a new debug-only fault switch, `INFRA_FAULT_MEMSTREAM_FINISH=1`,
which makes every finish really close its stream and then report failure. It asserts four surfaces, not every site. The
`--pack-signatures` map and the `--json` map come out byte-identical to the undegraded run outside `est_tokens`,
well-formed, at exit 0. The `--token-budget` run and a `--from-trace` run each print 0 bytes and exit 1 where their
controls print the answer. Under the same switch, MCP `uses` answers `-32603`, and the `--for` redaction summary
matches its control in XML and `--json`. **#14g** reads `src/` and refuses an `open_memstream`, a direct call of the charge opener, or
an `fflush`/`fclose` of a memory stream anywhere outside the type. On `f8e6087c` it reports 46 such lines. Its positive
control puts the two lines of one site back by hand, once per spelling of the opener (bare, `::`, `os::`, `rw::os::`),
and must report exactly those two each time.

### Fixed — five code extensions the index parses were no language at all to the dependency, state and lint verbs

The crawl indexes `.metal`, `.cu` and `.cuh` as C++, `.pyi` as Python and `.phtml` as PHP. `langOfPath`
(`src/lintrules.h`) is the verb-time classifier that `--deps`, `--arch`, co-change's `dep_capable=`, `--nonlocal-state`,
`--quality-panel`, the lint catalog and user `--lint-rules` use to bucket a file. It kept its own extension table "in sync
by hand", that table had drifted, and it called those five extensions Unknown. Every one of those verbs quietly left the
files out. `includeLangOf` (`src/resolve.h`) had the same four C++ and Python gaps, so even a counted file could not
resolve its includes.

Both tables now know all five, and the crawl's table and `langOfPath`'s are asserted equal at compile time (above).
`test/deplangscheck.sh` arm (G) requires every dependency-counted extension to resolve too. It went red with only the
classifier rows added, naming `.cu`, `.cuh`, `.metal` and `.pyi` as counted but unresolvable (and `.hxx` the other way
round), which is why the resolver rows land in the same change. `.hxx` left both tables: the crawl admits no `.hxx` file, so neither row could ever be reached.

Measured by comparing stdout and exit code, `--no-cache`, between the base binary (`f8e6087c`) and this change, over all
162 fixture corpora under `test/` and eight verbs: 1,296 runs. 14 differ. All 14 are on the six corpora that hold one of
the extensions, and only on `--deps`, `--nonlocal-state` and `--quality-panel`. The map, `--json`, `--lint`,
`--lint-catalog` and `--pack-signatures` are byte-identical everywhere.
- `test/cudafix` `--nonlocal-state`: `cells="0" functions="0"` became `cells="5" functions="4"`. The CUDA kernel's
`rk_scaleTable`, read through `rk_clampScale`, was invisible.
- `test/cudafix` `--deps`: `dep_files="1"` became `3`, and the kernel's include of `reduceShared.cuh` now counts
(`afferent` 1 → 2, `transitive` 1 → 2). `test/metalfix` already printed the `.metal` shader's row. The shader now
counts in `dep_files` (2 → 3), and its quote include of `AAPLSharedTypes.h` resolves (the header's `afferent` 1 → 2).
`test/phpfix`, `pyshapefix`, `stdqualfix` and `macroreparsefix` each gain the one file their denominator was missing.
- `test/phpfix` `--nonlocal-state`: `unanalyzed_files="4"` became `5`. The `.phtml` view is disclosed as unanalysed PHP.
- A user rule with `language: cpp` run over a `.metal` shader and a `.cu` kernel reported `findings="0"`. It now reports 16.
- `.pyi` typing stubs. A stub restates its module's globals (`COUNT: int` beside `m.py`'s `COUNT = 0`), so reading
both files counted one global twice: a two-file probe went from `cells="1"` to `cells="2"` with every row still bound to
`m.py`. `--nonlocal-state` and `--quality-panel` now skip a stub whose `.py` is indexed beside it. A stub with no
source, the shape a C extension ships, is the only declaration of its module and keeps its cells: `test/pyshapefix`'s
`stubs.pyi` adds one (`cells` 5 → 6). Gate: `test/nonlocalstatecheck.sh` arm (J), red at `cells="2"` before the skip.

Dart stays outside the dependency denominator, now by a named case instead of a `default:`. Dart has no import capture,
and a two-file probe showed `--deps` printing no row for `import 'util.dart';`.

### Changed — CI runs a light set on push to main and on `train-member` pull requests; the full matrix moves to a nightly schedule and `workflow_dispatch`

CI was the bottleneck: a merge to main re-ran the full 31-job matrix on a tree its pull request had already
Expand Down
19 changes: 19 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,25 @@ if(RIPWIRE_ASAN)
COMMENT "Run the G1 fixture with the platform leak-detection policy")
endif()

# ---- compile-time fences: two warnings that are errors on OUR C++ targets, every compiler, every flavour ----
# The tree passes no warning set (no -Wall), so until this block a check only a warning could make was a check nobody
# read, and on GCC -Wswitch did not run at all: GCC enables it only under -Wall, Clang by default.
# -Werror=switch a switch over an enum with no `default:` must name every enumerator, so appending
# one (a Lang, a SymKind, a RefRole) is a compile error at every per-value switch that
# has to decide it. The house rule that makes this bite: a switch returning one answer
# PER ENUMERATOR carries no `default:`, and ends with a plain `return` for a byte past
# the enum (a corrupt cache value); GCC's -Wreturn-type needs that line and Clang
# accepts it. A switch over a deliberate SUBSET keeps its `default:`.
# -Werror=implicit-fallthrough a case body that falls into the next label says so with [[fallthrough]].
# Both were measured at 0 diagnostics on this tree before they became errors (AppleClang and Homebrew clang, debug and
# -DNDEBUG), so they cost nothing today and fence the next edit. GCC's first run of them is CI's.
# OUR TARGETS ONLY, the same rule the sanitizer and separate-storage blocks follow: the tree-sitter core and the grammar
# objects are generated C we do not own, and their scanners fall through by design.
set(RIPWIRE_COMPILE_TIME_FENCE_FLAGS -Werror=switch -Werror=implicit-fallthrough)
foreach(_t IN LISTS RIPWIRE_OWNED_CXX_TARGETS)
target_compile_options(${_t} PRIVATE ${RIPWIRE_COMPILE_TIME_FENCE_FLAGS})
endforeach()

# ---- G-race ThreadSanitizer build (the qsnap-prefetch worker vs the request loop) ----
# A SEPARATE target/dir from ASan (TSan and ASan are mutually exclusive). Vets the one concurrent path in the
# server: the detached HEAD-snapshot prefetch thread against a request-thread ingest, serialized by
Expand Down
16 changes: 9 additions & 7 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,15 @@ already knew about the others, several while fixing one. So the rule is mechanic
until someone adds an early `return` above it. One owner whose destructor releases what it holds
collapses that to a single handler whose only job is the conversion this codebase actually wants: a
recoverable error becomes a degrade, returned, never propagated. Measured on `216802ad`, 2026-09-14:
of **27 `catch` blocks under `src/`, exactly one releases a resource by hand** — `infra/emit.h`'s
`renderToString`, which `fclose`s a memstream and `free`s its buffer. The other 26 convert a throw
into a degrade, set a flag, return a message, or `continue`; they own nothing, which is why they are
one line each. Re-derive rather than trust: a bare `grep -cE '\bcatch[[:space:]]*\('` over `src/`
reports **35**, and 8 of those hits are the word inside a `//` comment or inside a tree-sitter query
string — most of them in `lintrules.h`, whose subject is *detecting* empty catch blocks in other
people's code. Exclude comment and string context, then read each surviving handler's first body
of 27 `catch` blocks under `src/`, exactly one released a resource by hand — `infra/emit.h`'s
`renderToString`, which `fclose`d a memstream and `free`d its buffer. Re-derived after that buffer
moved into `rw::MemoryStream` (2026-09-16, `lane/compile-time-checks`): **26 `catch` blocks, and none
releases a resource by hand** — `renderToString`'s two handlers now leave the stream and its buffer to
the owner's destructor. Every handler converts a throw into a degrade, sets a flag, returns a message,
or `continue`s; they own nothing, which is why they are one line each. Re-derive rather than trust: a
bare `grep -cE '\bcatch[[:space:]]*\('` over `src/` reports **34** there, and 8 of those hits are the
word inside a `//` comment or inside a tree-sitter query string — most of them in `lintrules.h`, whose
subject is *detecting* empty catch blocks in other people's code. Exclude comment and string context, then read each surviving handler's first body
line, because the resource question is answered by reading it and not by counting.

### Naming encodes what the type cannot
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1817,9 +1817,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published
### In the tests

<details>
<summary><b>621 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->
<summary><b>622 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->

`test/regression.sh` names **621 gate scripts** and is the authoritative list; <!-- gatecount -->
`test/regression.sh` names **622 gate scripts** and is the authoritative list; <!-- gatecount -->
`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
Expand Down
6 changes: 3 additions & 3 deletions docs/EVALS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` | 621 gate scripts plus the determinism, cache-transparency and golden contracts. <!-- gatecount --> |
| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 622 gate scripts plus the determinism, cache-transparency and golden contracts. <!-- gatecount --> |
| **`--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)
Expand Down Expand Up @@ -5837,7 +5837,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 **621 gate scripts**, all of which exist on disk. <!-- gatecount -->
naming **622 gate scripts**, all of which exist on disk. <!-- gatecount -->

`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`.
Expand Down Expand Up @@ -6849,7 +6849,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 621. The <!-- gatecount -->
~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 622. The <!-- gatecount -->
loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not
written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every
published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh`
Expand Down
Loading