28866ac made `centroidStatus` name why centring is disabled. Nothing read it.
`stats()` reported `centroid` from `loadCentroid` — the raw row, which says WHAT
the centre is and not WHY centring is or is not happening — and the only two
callers of `centroidStatus` in `lib/` were `centroidRecallState` (which spreads
the result but is itself only used by tests) and `#ensureCentroid` on the write
path, which discards the return value. A grep for `.reason` under `lib/` returned
nothing but the definition itself.
Same failure as D2 one commit earlier: the value existed, was covered, and no
operator could see it.
## What this adds
`stats().centroid_state` — the verdict, alongside the existing raw `centroid`:
centroid the stored row (dim, mean, vectorCount, stats)
centroid_state { available, stale, reason, delta, rebuildScheduled, ... }
reason ∈ ok | no_centroid | dim_mismatch | zero_sample |
stale | isotropic
`centroid` keeps its exact previous shape, so nothing downstream changes.
## The part that is easy to get wrong
`centroidStatus` **registers a rebuild** when it judges a space stale. A stats
read must not mutate anything, so the call passes `schedule: () => {}` — with
the no-op scheduler `rebuildScheduled` is always false here, which is what a pure
read should report. Without it, every operator polling `stats()` on a drifted
space would keep the rebuild loop alive.
## Criterion
`test/phase-b-c5-reasons.test.js` gains a ninth case driving the real plugin:
fresh space -> reason 'no_centroid'
after 8 commits + wait -> 'ok', available true, raw row and verdict both present
drift past the ratio -> 'stale', stale true, rebuildScheduled FALSE
The last assertion is the one that matters: it distinguishes "reports staleness"
from "acts on it".
Mutation — deleting the `centroid_state` field:
not ok 9 - C5: stats() exposes the reason without scheduling a rebuild
error: "Cannot read properties of undefined (reading 'reason')"
Also removed a tautology I had written in the same case
(`assert.equal(s.centroid, s.centroid, ...)`) — it can never fail, and a criterion
that cannot fail is worse than none.
Gates: npm test 300/300 (0 skipped), npm run check 12/12.