The C4 slice instruction said, verbatim: "判 stale 的那一轮仍返回结果(用未校正向量)".
It was not implemented. `grep stale lib/recall.js` returned one comment and no code,
and `loadCentroid` carries no staleness verdict — so recall centred with whatever
mean it found, however old.
## Why not simply ask on the recall side
The obvious fix — call `centroidStatus` in `recallBlocks` — was built and measured:
# tests 305 # pass 304 # fail 1
not ok 97 - C3-k: the recall hot path must not run a COUNT query
error: 'lib/recall.js must not call centroidStatus — it runs countVectored
internally, and the recall path may only read the centroid'
C3-k is a rule this project set deliberately. A fix that breaks it is not a fix,
and the criterion caught the attempt — which is what it is for.
## What this does instead
The WRITE path already pays for the judgement on every commit (`#ensureCentroid`
calls `centroidStatus`). So it persists the verdict:
· `BlockStore#markCentroidStale(fp)` — `json_set(stats, '$.stale', 1)` when the
space is judged stale
· `#ensureCentroid` marks on `status.stale === true`
· `recallBlocks` honours it: `!centroid.stats?.stale` before centring
· cleared implicitly — `rebuildCentroid` → `saveCentroid` upserts stats without
the key
Recall never runs a COUNT; C3-k stays green.
## The bug this shipped with for one revision — and why it matters
The guard was first written `centroid.stats?.stale !== true`. The flag is stored as
the JSON number `1`, and in JS **`1 !== true` is true** — a strict comparison across
types — so the guard was a permanent no-op. Stale centroids kept being used.
**The entire suite was 305/305 green with that bug in place.** Nothing covered the
path. It was only visible on a fixture where centring is observable (the C4
hub/target space):
fresh centroid top1=target 0.9000 / 0.2129 / 0.1814
marked stale top1=hub 0.9928 / 0.9859 / 0.9803 <- raw cosine, uncorrected
after rebuild top1=target 0.9000 / 0.2129 / 0.1814
Hence C4j (below) rather than a comment claiming it works.
## Criterion C4j
Three assertions on that fixture: fresh → target; marked stale → hub AND the score
is the raw cosine (0.9928); rebuild → target again. Mutation — deleting the guard:
not ok 11 - C4j: a centroid marked stale falls back to uncorrected vectors
error: ... If this still says "target", the flag is being ignored — the exact bug
that a strict `!== true` comparison produced while the whole suite stayed green.
'target' !== 'hub'
## What this does NOT establish
That falling back improves ranking quality. An attempt to measure it could not
separate the cases: with a near-duplicate late corpus, scores saturate near 0.99
whether the mean is stale (0.997) or absent (0.994); only a freshly rebuilt mean
spread them (0.978..0.970). So this is a safety choice — a mean built from a
fraction of the space describes a space that no longer exists, and applying it is
unjustifiable on principle. The measurable win belongs to keeping the centroid
FRESH (the throttle in C3), not to skipping it.
Gates: npm test 306/306 (0 skipped), npm run check 12/12, frozen faces untouched.