07f8438 gave the three gates a counter and claimed "every drop is countable". A
probe against the invariant found a fourth drop site, one stage earlier than any
gate:
scoreVectorCandidates skips a row it cannot score
· unparseable JSON
· wrong width (CENTER width vs query)
· non-finite component
· zero norm
· centring collapsed it onto the space mean (C4's dust guard)
Measured on three identical blocks with centring active:
hits=0 skipped=0 skippedDim=0 skippedScore=0 skippedNoEvidence=0 truncated=0
accounted = 0 of 3 -> INVARIANT BROKEN
The whole space vanished while every counter read zero. Cause: a skipped row never
enters `vectorScores`, so `recallBlocks` never puts it in the candidate pool
(`if (!symbolMatch && !vectorSource) continue`), so no downstream counter can see
it. This is exactly the silent branch D2 forbids — deeper than where D2 first
looked, and I only found it because I ran the invariant against a centring case
instead of trusting the green suite.
## Fix
`skippedUnscoreable` threaded from `scoreVectorCandidates` (by reference, so the
early `no_vector_space` return can report it too) through `recallBlocks` into the
result, then into `last_recall` and the log line — the same three surfaces D2
already uses.
hits=0 skippedUnscoreable=3 ... accounted = 3 of 3 -> INVARIANT HOLDS
## Criterion
`test/recall-accounting.test.js` gains case 5: three identical blocks, centring
engaged, asserting the count is 3 (not 0) and that hits + all seven counters
still equals the space.
The existing invariant case now sums seven counters instead of six — a shortfall
is how this hole would reappear.
Mutation — deleting the dust increment:
not ok 5 - D2: a vector that centring collapses is counted, not silently lost
error: D2: these blocks were dropped before the candidate pool existed, so no
other counter can see them. ...
## Note on the label
These blocks are NOT counted as `skippedNoEvidence`: that would say "there was
nothing to score" when the truth may be "there was a vector and centring destroyed
it". Different remedies (re-embed vs. look at the centroid), so a separate
counter — the same reasoning that made C5 split its six reasons.
Gates: npm test 303/303 (0 skipped), npm run check 12/12.