176 — The clauses no row ever called
2026-09-22 · data lane · BACKLOG 11k-w-a-i
Ten days ago a session went to add .partial() to zod LF1 and found something worse on the way past: .omit() had been named in that fact's statement since the day the fact was filed, and no probe had ever called it. Not once, across two full ladder re-runs, a fact-citation sweep and a correction to the fact itself. Nothing in the repository was capable of noticing, and JOURNAL/172 wrote down why — every instrument here checks a row's boundary against its fact, or a run record's version against its fact. None checks whether a fact's clauses all have rows. JOURNAL/072's rule — write the probe from the fact's whole statement, split at every and, not from its headline — has been in CLAUDE.md for two weeks and is enforced by nobody.
This session built the thing that notices. It is tools/audit/clause-coverage.mjs: offline, writes nothing, installs nothing, and reports rather than gates.
What it does, and the one join it makes
For each fact carrying an introduced_in, take the surfaces its own api and statement name, and ask whether any of that fact's probe rows touches them.
Tokens come only from inside backticks. Prose is English, and an identifier-shaped word in a sentence ("use the string form") is not a surface. Inside a span it takes .method() calls, bare call() names, .property reads, object keys, and a span that is a single bare identifier.
A row's text is fn.toString() — what it executes. Comments are stripped first, deliberately: a clause named in a // line above the call that was never written must not read as covered, and that is very nearly the defect this file exists for. String literals are kept, because a prisma row touches previewFeatures by writing it into a schema file rather than by calling it. Module-level bindings the row reaches are followed transitively, since most prisma rows are assembled out of const schema fragments.
The match is on the bare identifier at a word boundary. That is loose in the safe direction: it can call a gap covered that is not, which costs a missed report rather than a false accusation. The motivating case survives it easily — the string omit appeared nowhere in LF1's closure.
Three absences, kept apart
The charter's rule about identifiers.mjs is that an instrument which reads its own blindness as a result is worse than no instrument. Three things here look alike in a count and are not:
| meaning | |
|---|---|
| no probe file | langchain, next.js and tailwindcss have no probes/<lib>.mjs at all — 100 of the corpus's 226 facts. They are unmeasured, not uncovered, and printing them as gaps would bury the five libraries that can actually answer. |
| unprobeable | the probe file names the fact in its unprobeable map with an argued reason. Counted, never reported. Six facts. |
| no rows | dated, has a probe file, and no row claims it. A whole fact unprobed is a larger hole than a missing clause, so it prints first. Zero of these, which is a genuinely good reading. |
The first run
226 facts, 224 dated, 118 with rows, 6 unprobeable, 0 dated-with-no-row
550 tokens named, 380 touched by a row, 170 not
Split three ways: 122 CLAIM, 29 CROSS, 19 ADVICE.
CROSS is the class the first run taught me existed: the surface is executed on this ladder, by a sibling fact's rows. zod LF1's statement names .required() in order to say it went the other way at the same release, and LF45's row is what calls it. That is a fact citing a fact, not an unprobed clause — the proposition is measured, the attribution is just elsewhere. ADVICE is a token named in a sentence saying what to do instead; a correction is not usually what introduced_in dates. Both are printed rather than suppressed, and both are weak evidence.
Four phantoms the first run killed
The run before these fixes reported 171. Forty-nine of those were the extractor's fault, in four systematic shapes — all found by looking at the output rather than by reasoning about it:
prisma.config.tsread as.configand.ts;*.vercel.appandmyapp.comread as.vercel,.app,.com. Filenames and hosts are now masked by the only test that separates them from a real property read: the last segment. A path or a host ends in a file extension or a TLD; a surface does not end in.ts. The lists are short and closed, so an unknown tail leaves the span alone — noise rather than a missed gap.library/src/actions/index.tssplit into four phantom surfaces, because the corpus writes "this fact covers these names" astoNumber / toBoolean / toDateand a path uses the same character. The slashes must be spaced. The corpus is consistent about this and it holds.TS2554:read as a surface. A TypeScript diagnostic code is the compiler's identifier and no probe will ever contain one.object,string,booleandropped as type keywords — and this one was caught by the selftest, not by the run. In a schema library the primitive names are the surface:z.object()andv.boolean()are among the most-named APIs in the corpus, whilesuccess: booleanis an annotation. They are now dropped for every shape except a call. What separates the API from the type is the parenthesis.
Four gaps read by hand, of which three were real
An instrument whose output nobody has read is exactly what this repository has twice refused to gate on, so four entries were checked against the probe source:
react-router LF2 — real, and it is LF1's shape exactly. The statement opens "loader, action and middleware receive a url argument since 7.15.0". It has three rows. All three exercise loader. action and middleware have never been called. This is precisely the split-at-every-and rule, violated in the first sentence of the fact.
prisma LF14 — real. The statement names four engine classes (LibraryEngine, BinaryEngine, DataProxyEngine, ReactNativeEngine) and four environment variables (PRISMA_CLIENT_ENGINE_TYPE and its three siblings). It has one row, which reads engineType.
valibot LF2 — real, and the most interesting of the three. The fact is about the emoji validation action carrying a ReDoS regex fixed in 1.2.0. Its row probes v.EMOJI_REGEX — the exported constant behind the action — and times a catastrophic input against it. The boundary it finds is probably right, but the row would still read true if the action stopped using that regex, which is the one way the fact could become false without the row noticing. The statement's subject and the row's subject are not the same object.
zod LF36 ZodError — noise, a type name in prose. This is the class the file promises it will produce and the reason it does not gate.
What was deliberately not done
No row was written and no fact was corrected. The first run is the measurement of how big the gap is; the rows it asks for are the next chunk, and they are cheap where a ladder is on disk (zod 21 rungs, prisma 38, valibot 11 — all three survived the reboot, checked in JOURNAL/174). Writing them changes boundaries, which needs a bisect run, which is its own session.
It was not put in the pre-commit chain, in the same session that wrote it, on purpose. BACKLOG 11k-w-a-i said not to and the reason is the corpus's own history: a gate whose output nobody has triaged has been refused twice here. 122 open claims is not a chain a commit can wait behind, and the right sequence is triage first, then decide. The ALLOW list ships empty for the same reason — every entry in it is a claim about the data, and none has been earned yet.
The selftest is 45 checks and drives the extractor, the comment stripping, the binding closure (including a cycle), the row-attribution rule (LF1b belongs to LF1; LF10 does not), and the motivating case in both states — LF1 as it was filed, where omit must be reported, and LF1 as it is now, where it must not.
What it still cannot do
A clause with no identifier in it is invisible here. "None of the three silently drops the refinement any more", "the message names the method it was called as" — there is no token to join on, and those are exactly the clauses JOURNAL/072 was written about. This finds the named surface a row never touches. It does not find the unnamed proposition a row never asserts. That is still found by reading.
No count moved: 166 runs / 168 findings (161 chargeable) / 8 libraries, 226 facts. No fact filed, no finding charged, no date moved, no probe changed. No money moved; LEDGER.md untouched. Nothing published, listed, posted, submitted or sent. No throttling observed.
Next data-lane chunk: the rows this run asks for, starting with react-router LF2's action and middleware — except that react-router's ladder is the one that is not installed, so the cheap start is prisma LF14 and valibot LF2, whose ladders are.
Gates for Sam: unchanged. BACKLOG 0c (the valibot disclosure), D2's four, D3's four, D5's one-POST IndexNow ask, D6's two.