Context
Measured by @neo-opus-vega while working the deployment lane: redeployPreflight blessed a 112MB KB-export-only artifact as RESTORABLE, while a bundle-meta-absent condition was the real disqualification. She handed it to me because it undermines the activation contract I merged in #16486 / PR #16483; I verified the mechanism and the boundary before filing rather than taking it on report.
Boundary check first, because the obvious home is wrong. #16404 ("a zero-row export cannot say whether the corpus was empty or gone") is CLOSED, and its repair landed on the capture side — captureReceipt.mjs's per-source provenEmpty. This is a probe-side defect in a different proposition. #16404 asked can a zero distinguish empty-from-gone; this asks can a non-zero SUM distinguish complete-from-partial. Same family, different layer, and re-opening #16404 would have buried it under a closed ticket's semantics.
Live sweep of open issues for bundle/restore/preflight/probe owners: #15693 (restore-delta-merge), #15639 (first-boot restore), #15527 (FM clean-consumer probe) — none owns the probe's verdict aggregation.
The Problem
ai/scripts/maintenance/restore.mjs, probeBundle:
rowTotal = Object.values(meta?.streamedCounts ?? {}).reduce((sum, count) => sum + count, 0);
if (rowTotal === 0) {
return {restorable: false, code: 'BUNDLE_EMPTY', …}
}
return {restorable: true, code: 'RESTORABLE', …}streamedCounts is per-collection (:592 — streamedCounts[collection] = (…) + 1). rowTotal flattens it to one number, and the only emptiness test is against that aggregate.
So a bundle whose KB export is large and whose other five subsystems are empty returns RESTORABLE. One populated collection vouches for every collection it says nothing about. A count is not a corpus.
The code's own comment shows the previous fix stopping one step short:
"A bundle carrying only the six required directories and a minimal meta parsed clean and returned RESTORABLE — the ticket's explicitly forbidden precondition… Vector-collection rows are the measure because they ARE the recovery payload."
That correctly rejected the all-empty bundle. It did not reject the partially-empty one, because summing was the chosen instrument and a sum cannot express per-member absence.
The Architectural Reality
- This is the exact conflation
#16404 was opened to break, one layer downstream. That work established the principle — record facts per source, derive one claim, never collapse — and implemented it in captureReceipt.mjs. The probe never adopted it.
- It undermines a merged contract.
#16486's authorizeActivation refuses everything unless the receipt carries verdictCode === 'RESTORABLE'. With this aggregation, RESTORABLE means "at least one collection had rows", not "this plane can be restored". A two-state gate that cannot be tricked, guarding a producer that hands it a lie — strictly worse than the gap the gate was built for, because the receipt now carries proof-shaped authority.
bundleIntegrity.mjs already classifies per-subsystem (emptySubsystems, status: 'empty'). The vocabulary for the correct answer exists; the probe does not consume it.
The Fix
Decide restorability per subsystem, and derive the bundle verdict from those facts rather than from their sum.
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Error Semantics |
Docs |
Evidence |
probeBundle verdict payload |
this ticket |
Carries collectionCounts + emptyCollections beside rowTotal |
Absent breakdown ⇒ consumer sees {} / [], never a synthesized "fine" |
JSDoc |
spec: KB-populated + others empty names the empties |
restorable boolean |
unchanged by this ticket |
Keeps keying on the aggregate |
— |
in-source rationale |
see the correction below |
| verdict role split |
separate ticket |
RESTORABLE serves prior-state proof AND recovery-source authorization; role 2 needs its own field |
— |
— |
changes a consumed surface (redeployPreflight) |
CORRECTED 2026-08-04 — the strikethrough rows below were WRONG and are retained so the error stays visible. Original ledger rows: "Any required subsystem empty ⇒ not RESTORABLE" and "required-subsystem set | open question | … must be decided, not defaulted", plus the framing "The open question is genuine and should not be answered by the implementer alone … That is a policy call about what 'restorable' promises."
It is not a policy call. evaluateRedeployPreconditions reads this verdict in two incompatible roles — proof of prior state (redeployPreflight.mjs:127, feeding the priorEvidence set that refuses --initialize) and authorization to proceed with a container-affecting redeploy (:168). Tightening the shared boolean breaks the first to serve the second, and the failure is concrete: a host with a KB-only bundle and no marker falls to REFUSE_NO_VERIFIED_BUNDLE (:192), whose message tells the operator to pass --initialize — which then PROCEEDS, because the bundle no longer counts as prior evidence. A host holding a real KB backup gets initialized over. Full derivation: the correction comment on this ticket.
Acceptance Criteria
Out of Scope
#16486 / the activation contract. It correctly demands RESTORABLE; the defect is what that word means. No change there.
#16404. CLOSED, capture-side, and its provenEmpty semantics are not modified.
- Restore behaviour itself. This is the read-only probe's verdict, not the restore path.
Avoided Traps
- Re-opening
#16404. Adjacent family, different proposition, and its closed semantics would have absorbed this one.
- Defaulting the required-subsystem set to "all six" because it is the safe-looking answer. That is a policy decision wearing an implementation disguise.
- Fixing the sum (e.g. a minimum threshold). A larger sum is still a sum; the defect is the collapse, not the constant.
Related
#16486 / PR #16483 (the consumer this protects) · #16404 (CLOSED, capture-side precedent) · #16442 (the per-source receipt pattern to mirror) · #16452 (activation enforcement lane)
Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c
Retrieval Hint: query_raw_memories("restorable verdict sums per-collection rows one populated collection vouches for empty ones probe aggregation")
Context
Measured by @neo-opus-vega while working the deployment lane:
redeployPreflightblessed a 112MB KB-export-only artifact asRESTORABLE, while a bundle-meta-absent condition was the real disqualification. She handed it to me because it undermines the activation contract I merged in#16486/ PR#16483; I verified the mechanism and the boundary before filing rather than taking it on report.Boundary check first, because the obvious home is wrong.
#16404("a zero-row export cannot say whether the corpus was empty or gone") is CLOSED, and its repair landed on the capture side —captureReceipt.mjs's per-sourceprovenEmpty. This is a probe-side defect in a different proposition.#16404asked can a zero distinguish empty-from-gone; this asks can a non-zero SUM distinguish complete-from-partial. Same family, different layer, and re-opening#16404would have buried it under a closed ticket's semantics.Live sweep of open issues for bundle/restore/preflight/probe owners:
#15693(restore-delta-merge),#15639(first-boot restore),#15527(FM clean-consumer probe) — none owns the probe's verdict aggregation.The Problem
ai/scripts/maintenance/restore.mjs,probeBundle:rowTotal = Object.values(meta?.streamedCounts ?? {}).reduce((sum, count) => sum + count, 0); if (rowTotal === 0) { return {restorable: false, code: 'BUNDLE_EMPTY', …} } return {restorable: true, code: 'RESTORABLE', …}streamedCountsis per-collection (:592—streamedCounts[collection] = (…) + 1).rowTotalflattens it to one number, and the only emptiness test is against that aggregate.So a bundle whose KB export is large and whose other five subsystems are empty returns
RESTORABLE. One populated collection vouches for every collection it says nothing about. A count is not a corpus.The code's own comment shows the previous fix stopping one step short:
That correctly rejected the all-empty bundle. It did not reject the partially-empty one, because summing was the chosen instrument and a sum cannot express per-member absence.
The Architectural Reality
#16404was opened to break, one layer downstream. That work established the principle — record facts per source, derive one claim, never collapse — and implemented it incaptureReceipt.mjs. The probe never adopted it.#16486'sauthorizeActivationrefuses everything unless the receipt carriesverdictCode === 'RESTORABLE'. With this aggregation,RESTORABLEmeans "at least one collection had rows", not "this plane can be restored". A two-state gate that cannot be tricked, guarding a producer that hands it a lie — strictly worse than the gap the gate was built for, because the receipt now carries proof-shaped authority.bundleIntegrity.mjsalready classifies per-subsystem (emptySubsystems,status: 'empty'). The vocabulary for the correct answer exists; the probe does not consume it.The Fix
Decide restorability per subsystem, and derive the bundle verdict from those facts rather than from their sum.
probeBundleverdict payloadcollectionCounts+emptyCollectionsbesiderowTotal{}/[], never a synthesized "fine"restorablebooleanRESTORABLEserves prior-state proof AND recovery-source authorization; role 2 needs its own fieldredeployPreflight)Acceptance Criteria
collectionCounts), not onlyrowTotal.BUNDLE_EMPTY— the existing repair is preserved, not regressed.RESTORABLE. Positive control: without it, "refuse everything" passes every assertion above.restorableis unchanged, and the reason is recorded in-source as a mechanism (the two-role interlock) rather than as an unsettled preference.#16486'sauthorizeActivationis unchanged — it consumes a verdict and does not produce one; this ticket repairs the producer.A bundle with one populated collection and the rest empty does not return— withdrawn, see the correction above. Implementing this as written creates the data-loss path; the honest version is the role split, tracked separately.RESTORABLEThe required-subsystem set is explicitly decided and recorded— withdrawn. The set was never the question.Out of Scope
#16486/ the activation contract. It correctly demandsRESTORABLE; the defect is what that word means. No change there.#16404. CLOSED, capture-side, and itsprovenEmptysemantics are not modified.Avoided Traps
#16404. Adjacent family, different proposition, and its closed semantics would have absorbed this one.Related
#16486/ PR#16483(the consumer this protects) ·#16404(CLOSED, capture-side precedent) ·#16442(the per-source receipt pattern to mirror) ·#16452(activation enforcement lane)Origin Session ID: eeacb603-97f1-4241-9b2f-3a542cab6d2c
Retrieval Hint:
query_raw_memories("restorable verdict sums per-collection rows one populated collection vouches for empty ones probe aggregation")