Repository navigation
Open
Labels
agent::claude-codedepends-on:3035doingThe work step of the issue loop is on itThe work step of the issue loop is on itepic:3029lib:service-backupphase:createdtype:change
Milestone
Description
Problem
Parent epic: faberline/axiom#3029. Execution position: 03 of 03.
A valid published BackupSet still needs a bounded, restart-safe path from
object storage to a service-owned restore. Fetching everything into memory or
partially applying before compatibility checks can make a fresh instance ready
with incomplete or incompatible state.
Capability Alignment
Capability: Versioned Stateful-Service Backup and Restore.
Capability Gap: no generic set discovery, validation, disk staging, resume, or
service-owned compatibility/apply boundary.
Progress Evidence: staged restore fault tests resume without reapplying
completed artifacts and cannot signal readiness before complete validation and
service acknowledgement.
Requirements
- R1: Resolve an explicit manifest or latest-success pointer, fetch the
immutable manifest, and validate version, inventory, size, and digest before
any service apply begins. - R2: Stream artifacts to a bounded local staging directory using atomic
per-artifact completion markers; never require the full set in memory. - R3: Persist restore progress so interruption resumes verified artifacts and
never confuses a partial file with a completed one. - R4: Expose a service adapter that validates domain metadata and applies the
complete staged set;service-backupdoes not know shard, membership, index,
queue, or workflow semantics. - R5: Fail closed on missing, duplicate, corrupt, incompatible, or changed
objects and keep the consumer unready until the service reports success. - R6: Define idempotence for replaying the same set and reject an unsafe attempt
to apply a different set over an in-progress or committed restore.
Scope
In Scope
- Manifest resolution, download/validation, staging layout, restart progress,
cleanup, service adapter boundary, readiness outcome, and fault tests.
Out of Scope
- Service-domain restore implementation, Raft membership rebinding,
Kubernetes empty-PVC orchestration, incremental restore, and online rollback.
Acceptance Criteria
- AC1: Large multi-artifact sets restore with bounded memory and verified
on-disk staging. - AC2: Crash after every download/verify/apply boundary resumes exactly once or
remains fail-closed and unready. - AC3: Corrupt, incompatible, missing, duplicate, or replaced artifacts are
rejected before service apply. - AC4: Two example adapters with different metadata prove the shared layer
contains no service-domain assumptions.
Reference Context
Related Specs
| Spec | Relevance |
|---|---|
libs/service-backup/src/lib.rs |
current source contract |
libs/storage-durable |
atomic local staging primitives |
| faberline/axiom#3029 | parent lifecycle contract |
Spec Plan
| Spec ID | Action | Main Spec Ref |
|---|---|---|
service-backup-set-restore |
create | staging, resume, and adapter contract |
Verification Inventory
| Requirement | Gate | Oracle | Depends On |
|---|---|---|---|
| R1 | cargo test -p service-backup --all-features |
explicit/latest manifest validation cases pass | - |
| R2 | cargo test -p service-backup --all-features |
large-set bounded-memory staging cases pass | R1 |
| R3 | cargo test -p service-backup --all-features |
crash-resume markers never accept partial files | R2 |
| R4 | cargo test -p service-backup --all-features |
multiple domain adapters compile and receive only complete staged sets | R3 |
| R5 | cargo test -p service-backup --all-features |
corruption/incompatibility keeps readiness false and skips apply | R1 |
| R6 | cargo test -p service-backup --all-features |
same-set replay is idempotent and conflicting-set replay is rejected | R4 |
Metadata
Metadata
Assignees
Labels
agent::claude-codedepends-on:3035doingThe work step of the issue loop is on itThe work step of the issue loop is on itepic:3029lib:service-backupphase:createdtype:change
Activity
chrischeng-c4 commentedon Oct 23, 2026
Scope of this spec: the first PR of #18 only — R3 and R6 as pure domain logic (the persisted restore progress ledger and its transitions), plus the part of AC2 that a pure state machine can prove. Manifest resolution (R1), disk staging and download (R2), the service adapter (R4), readiness wiring (R5) and AC1/AC3/AC4 need later specs on this issue, after #16's streaming traits and
BackupSetV1are onmain. This PR adds types only, performs no I/O, and changes no existing public item. It does not depend on #16: its inputs are plain strings and integers.Goal
A restore that is interrupted at any step must resume without re-applying finished work and without mistaking a half-written file for a finished one. Add one pure domain type,
RestoreLedger, that records which set is being restored and the state of each artifact, enforces the legal transitions, serializes to one canonical JSON document for the later staging code to persist, and refuses to start a different set over an in-progress or committed restore. Later PRs of #18 persist it atomically (throughstorage-durable) and drive it from the download, verify and apply steps.Acceptance
crates/service-backup/src/domain/(one main type per file):restore_ledger.rs(RestoreLedger,ArtifactStage,RestorePhase),restore_ledger_error.rs(RestoreLedgerError, athiserrorenum with one variant per failure below). Declare them insrc/domain.rsand re-export fromsrc/lib.rsnext to the current exports.RestoreLedger::begin(set_id: &str, manifest_digest: &str, artifacts: &[(String, u64)]) -> Result<Self, RestoreLedgerError>:set_idis non-empty, at most 128 bytes, of[A-Za-z0-9._-];manifest_digestis"sha256:"plus 64 lowercase hex chars;artifactsis a non-empty list of(name, size_bytes)with unique non-empty names, at most 1024 entries. Every artifact starts atArtifactStage::Pending, the phase atRestorePhase::Staging.ArtifactStageisPending,Verified,Applied;RestorePhaseisStaging,Applying,Committed. Transitions, each a method returningResult<(), RestoreLedgerError>:mark_verified(name)(Pending→Verified; repeating it onVerifiedorAppliedisOkand changes nothing),begin_apply()(Staging→Applying, only when every artifact isVerified; repeating it whileApplyingisOk),mark_applied(name)(only inApplying,Verified→Applied; repeating onAppliedisOk),commit()(Applying→Committed, only when every artifact isApplied; repeating onCommittedisOk). An unknown artifact name, a skipped step, or a transition from the wrong phase returns the matching error variant and leaves the ledger unchanged.RestoreLedger::resume(&self, set_id: &str, manifest_digest: &str) -> Result<ResumePoint, RestoreLedgerError>withResumePoint::{Fresh, Continue, AlreadyCommitted}: the sameset_idandmanifest_digestgiveAlreadyCommittedwhenCommitted, elseContinue; a differentset_id, or the sameset_idwith another digest (a replaced manifest), returnsRestoreLedgerError::ConflictingSetcarrying both ids, and never changes the ledger. (Freshis returned by a free functionRestoreLedger::resume_or_begin(existing: Option<Self>, set_id, manifest_digest, artifacts) -> Result<(Self, ResumePoint), RestoreLedgerError>whenexistingisNone.)pending(&self) -> Vec<&str>(artifacts stillPending, in manifest order),unapplied(&self) -> Vec<&str>(artifacts not yetApplied, in manifest order),phase(),set_id(),manifest_digest()accessors.to_json(&self) -> Vec<u8>writes compact JSON with a"version": 1field, artifacts in their original order;RestoreLedger::from_json(bytes: &[u8]) -> Result<Self, RestoreLedgerError>refuses input over 1 MiB before parsing, unknown fields, unsupported versions, and any document that violates the invariants ofbeginor of the phase (for exampleCommittedwith an artifact notApplied, orStagingwith anAppliedartifact). Serde usescamelCaseanddeny_unknown_fields.from_json(to_json(x))round-trips.resumewith the same ids, replay the whole sequence (idempotent repeats allowed), and reachCommittedwith each artifact applied exactly once. "Applied exactly once" is proved by counting themark_appliedcalls that changed state.serdeandserde_jsonas already declared incrates/service-backup/Cargo.toml.crates/service-backup/tests/it/restore_ledger.rs, registered intests/it/main.rswithout a feature gate, with one case each for: the valid full path; each invalidbegininput; each illegal transition; idempotent repeats;ConflictingSetfor a different set and for a replaced manifest;AlreadyCommittedreplay; the JSON round trip; each rejected JSON document (oversized, unknown field, version 2, inconsistent phase); and the crash-prefix test above.Never
BackupSink,SnapshotSource,run_backup_once,fetch_backup_object, or any sink/source adapter.std::fs,SystemTime::now,Utc::now);ddd.toml[policy.domain]forbids them. The later infrastructure PR owns persistence.schemars::JsonSchemaon the new types, and do not changetests/it/schema_golden.rsortests/it/wire_golden.rs.BackupSetV1or any other unmerged [01/03] service-backup: define streaming BackupSetV1 manifest and single-object compatibility #16 or [02/03] service-backup: publish immutable BackupSets manifest-last and retain complete sets #17 type.crates/service-backup/Cargo.toml.README.md,llms.txt,src/api/llm.rs, ordocs/in this PR.ddd.toml, or CI.Gate
chrischeng-c4 commentedon Oct 24, 2026
Work submitted in #64; waiting for required CI on the current head.