Context
backup.mjs verifyBundleIntegrity (the row-count parity check added with #13584) compares the bundle's JSONL row count against the SDK source count and throws on mismatch — which correctly catches the #14042 false-green class (manifest claims N, artifact 0 bytes → N≠0 → fail).
The Problem
The parity branch if (bundleCount === sourceCount) reports status: 'pass' when both are zero. A backup of a gutted store (the #13999 signature: the source was already emptied → export returns 0 → bundle is 0) therefore reports a healthy pass, indistinguishable from a real backup. An all-empty "passing" backup is a false recovery source — exactly the trust gap the backup substrate is supposed to close.
The Fix
In verifyBundleIntegrity, distinguish status: 'empty' (sourceCount === bundleCount === 0) from a healthy pass (N === N, N > 0). In runBackup, surface empty subsystems with a loud logger.warn — non-fatal (a fresh environment legitimately backs up empty subsystems) but visible, and the empty status is carried in bundle-meta.integrity so a downstream canary/alert can escalate on it.
Acceptance Criteria
Relation
- Slice of #14030 AC2 (verify the backup is a usable recovery source, not merely present). The remaining #14030 AC2 work — a sampled logical import into a throwaway store — and AC1 (alert wiring) / AC3 (retention-SLA) follow separately per the build-order on #14030.
- #13999 — the gutted-store incident this makes visible.
Contract Ledger Matrix
| Consumed surface |
Field |
Allowed values |
Change |
Consumer impact |
Fallback / non-fatal semantics |
verifyBundleIntegrity() return + persisted bundle-meta.integrity[] |
status |
pass (positive row-count parity) · empty (source === bundle === 0 — non-fatal but recovery-unhealthy) · fail (row-count mismatch) · skipped (non-numeric source count) |
Adds empty (previously pass/fail/skipped) |
No current consumer switches on status: restore.mjs runs its own pre-flight validateBundle + reads only topology/neoVersion; HealthService reads only completedAt/timestamp. Intended consumer: the future backup canary (#14026 / #14030 AC1). |
Additive — an unknown-status consumer treats empty as not-pass (correct: empty ≠ healthy). runBackup surfaces empty non-fatally (a fresh environment legitimately backs up empty subsystems): a loud logger.warn + the empty status persisted into bundle-meta.integrity for the canary to escalate on. A populated deployment reporting empty is the gutted-store signature. |
- Docs:
verifyBundleIntegrity JSDoc (body + @returns) documents all four statuses.
- Evidence: L2 — the helper test covers the empty-parity branch; the
runBackup propagation test asserts status:'empty' is persisted into bundle-meta.integrity with a non-fatal warning.
Authored-by: @neo-opus-vega (Vega, Claude Opus 4.8)
Context
backup.mjsverifyBundleIntegrity(the row-count parity check added with #13584) compares the bundle's JSONL row count against the SDK source count and throws on mismatch — which correctly catches the #14042 false-green class (manifest claims N, artifact 0 bytes → N≠0 → fail).The Problem
The parity branch
if (bundleCount === sourceCount)reportsstatus: 'pass'when both are zero. A backup of a gutted store (the #13999 signature: the source was already emptied → export returns 0 → bundle is 0) therefore reports a healthypass, indistinguishable from a real backup. An all-empty "passing" backup is a false recovery source — exactly the trust gap the backup substrate is supposed to close.The Fix
In
verifyBundleIntegrity, distinguishstatus: 'empty'(sourceCount === bundleCount === 0) from a healthypass(N === N, N > 0). InrunBackup, surface empty subsystems with a loudlogger.warn— non-fatal (a fresh environment legitimately backs up empty subsystems) but visible, and theemptystatus is carried inbundle-meta.integrityso a downstream canary/alert can escalate on it.Acceptance Criteria
verifyBundleIntegrityreturnsstatus: 'empty'(not'pass') when source and bundle are both zero, with a reason.N === N(N > 0) still returns'pass'; a mismatch still returns'fail'; non-numeric still'skipped'.runBackupwarns (non-fatal) on empty subsystems;bundle-meta.integritycarries theemptystatus.Relation
Contract Ledger Matrix
verifyBundleIntegrity()return + persistedbundle-meta.integrity[]statuspass(positive row-count parity) ·empty(source === bundle === 0 — non-fatal but recovery-unhealthy) ·fail(row-count mismatch) ·skipped(non-numeric source count)empty(previouslypass/fail/skipped)status:restore.mjsruns its own pre-flightvalidateBundle+ reads onlytopology/neoVersion;HealthServicereads onlycompletedAt/timestamp. Intended consumer: the future backup canary (#14026 / #14030 AC1).emptyas not-pass(correct: empty ≠ healthy).runBackupsurfacesemptynon-fatally (a fresh environment legitimately backs up empty subsystems): a loudlogger.warn+ theemptystatus persisted intobundle-meta.integrityfor the canary to escalate on. A populated deployment reportingemptyis the gutted-store signature.verifyBundleIntegrityJSDoc (body +@returns) documents all four statuses.runBackuppropagation test assertsstatus:'empty'is persisted intobundle-meta.integritywith a non-fatal warning.Authored-by: @neo-opus-vega (Vega, Claude Opus 4.8)