| index 060884d1b..2112daa44 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md |
| @@ -750,7 +750,7 @@ Tests: `RenderHeld` writes the file for a `landing` row that `SyncExec` would sk |
| **Interfaces:** |
| - `moves.ReadDBHeader(path string) (pageSize uint32, pageCount uint32, err error)` (SQLite header bytes 16-17 and 28-31). |
| - `moves.EncodeFull(db io.ReaderAt, pageSize, pageCount uint32, min, max ltx.TXID, timestamp int64, walOffset int64) (data []byte, crc64 uint64, err error)`: `ltx.NewEncoder` with `Header{Version: ltx.Version, Flags: ltx.HeaderFlagNoChecksum, PageSize, Commit: pageCount, MinTXID: min, MaxTXID: max, Timestamp: timestamp, WALOffset: walOffset}`, every page in order **except SQLite's lock page**, `ltx.LockPgno(pageSize)` (`ltx.go:494`; the encoder refuses it, `encoder.go:219`, and litestream skips it), while `Commit` keeps the full page count; `Close`; the CRC-64 is the trailer's `FileChecksum` (read it back with `ltx.NewDecoder(bytes.NewReader(data)).Verify()` then `Trailer().FileChecksum`). |
| -- `moves.BuildIntent{V int; Op, Host string; N uint64; PageSize, Commit uint32; Timestamp int64}`; `moves.InstallManifest{V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time}` (**shared contract type**, coordination with claim Plan 6 Task 6, `feat/hostclaim-box` bf63e250: claim Plan 6 defines this exact shape in `internal/moves/install_contract.go`, beside `moves.ErrAttemptOver`, and is adding snake_case json tags before it merges: `v`, `op`, `host`, `n`, `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at` (platform-1e may rename `n` to `snapshot_txid` to match `generation.json`: follow the contract file as merged). Whichever lands on `main` first owns the type and the other imports it (the coordinator's rule). **If `internal/moves/install_contract.go` is on `main` when this task is built, import the existing `InstallManifest` and do not redefine it.** Either way `install.json` carries those snake_case keys, never Go's default field names: if this task defines the type, it adds exactly those tags; `TestInstallManifestKeys` decodes a built `install.json` into `map[string]any` and asserts the key set, so a drift from the contract fails here. Spec §7.6's `install.json` description names the same keys at its next revision); `moves.GenerationPrefix(location, op string) string` (`<location>g-<op>/`, location with its trailing slash); `moves.SnapshotKey(prefix string, n uint64)` = `prefix + "0009/" + ltx.FormatFilename(1, n)`; `moves.AnchorKey(prefix, n)` = `prefix + "0000/" + ltx.FormatFilename(n, n)`; `BuildIntentKey(prefix) = prefix+"build-intent.json"`, `InstallManifestKey(prefix) = prefix+"install.json"`. |
| +- `moves.BuildIntent{V int; Op, Host string; N uint64; PageSize, Commit uint32; Timestamp int64}`; `moves.InstallManifest{V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time}` (**shared contract type**, coordination with claim Plan 6 Task 6, `feat/hostclaim-box` bf63e250: claim Plan 6 defines this exact shape in `internal/moves/install_contract.go`, beside `moves.ErrAttemptOver`, with these snake_case json tags (final, from platform-1e): `v`, `op`, `host`, `snapshot_txid` (the Go field stays `N`), `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`; spec rev 7.22 §7.6 names the same keys. Whichever lands on `main` first owns the type and the other imports it (the coordinator's rule). **If `internal/moves/install_contract.go` is on `main` when this task is built, import the existing `InstallManifest` and do not redefine it.** Either way `install.json` carries those snake_case keys, never Go's default field names: if this task defines the type, it adds exactly those tags; `TestInstallManifestKeys` decodes a built `install.json` into `map[string]any` and asserts exactly that key set, so a drift from the contract fails here); `moves.GenerationPrefix(location, op string) string` (`<location>g-<op>/`, location with its trailing slash); `moves.SnapshotKey(prefix string, n uint64)` = `prefix + "0009/" + ltx.FormatFilename(1, n)`; `moves.AnchorKey(prefix, n)` = `prefix + "0000/" + ltx.FormatFilename(n, n)`; `BuildIntentKey(prefix) = prefix+"build-intent.json"`, `InstallManifestKey(prefix) = prefix+"install.json"`. |
| - `moves.NewSealedClient(store lease.VersionedStore, bucket, replicaPrefix string, seal Seal) *SealedClient` implementing `litestream.ReplicaClient`: `Type() "sealed"`, `Init` no-op, `LTXFiles(ctx, level, seek, _)` returns `ltx.NewFileInfoSliceIterator` over the sealed objects at that level parsed from their keys (`ltx.ParseFilename`), `OpenLTXFile` opens exactly the sealed version by `OpenVersion(ctx, bucket, key, versionID)` (ranged through `VersionRangeOpener` when offset/size are set), `WriteLTXFile`, `DeleteLTXFiles`, `DeleteAll` return `ErrSealedReadOnly`, `SetLogger` stores it. |
| - `moves.BuildGeneration(ctx, in BuildInputs) (InstallManifest, error)` with `BuildInputs{Store lease.VersionedStore; Bucket string; Source []SealedObject (the pinned version set: for a move the seal's replica/ objects, for region HA's relocation the versions a `genselect` plan or a current listing pins, L16); N uint64; GenerationPrefix string; Op, Host string; WorkDir string; Now func() time.Time; Verify func(dbPath string) error}` (`SealedClient` is built over `Source`, so nothing in the routine names a staging lane): (1) read or write the intent (`PutIfAbsent`; on `lease.ErrExists` read it and use its timestamp, page size, commit); (2) restore into a fresh scratch file: remove `<WorkDir>/<host>.db`, `-wal`, `-shm` and `-txid` first (litestream's `Restore` refuses an existing output path, `replica.go:601`; the file is reproducible from the sealed versions, so a replay after any crash starts clean), then `litestream.NewReplicaWithClient(nil, sealedClient).Restore(ctx, opt)` with `opt.OutputPath = <WorkDir>/<host>.db`, `opt.TXID = ltx.TXID(seal.N)`; refuse unless the restored position is `N` (litestream writes `<db>-txid`? Only in follow mode; instead compare the plan's final `MaxTXID` through `litestream.CalcRestorePlan(ctx, client, ltx.TXID(N), time.Time{}, logger)` whose last file's `MaxTXID` must be `N`, and verify every planned file with `ltx.NewDecoder(...).Verify()` before the restore); `Verify(dbPath)` = `lease.CheckDB` (integrity_check); (3) `EncodeFull` snapshot `[1, N]` and `PutIfAbsent(SnapshotKey)`; on `ErrExists`, `OpenVersion` the current one and require equal bytes (identical by construction from the intent; differing bytes refuse with `ErrBuildDiverged`); (4) the anchor `[N, N]` with `WALOffset = litestream.WALHeaderSize`, same rule; (5) sha256 of the database file; (6) `InstallManifest` via `PutIfAbsent` (on exists, read and compare `N`, keys, versions). |
| |
| @@ -793,7 +793,7 @@ Tests: `TestLocationParkIsAParkReplicaTheStoreAccepts` (a real `hostclaim.Update |
| - `parking`: list `holder.location` in the own bucket (`Store.ListVersions`); nothing there → done; else group the residue by generation as `park.Decide` does (every generation other than the one the landing will build; the origin as `@root`) and `hostclaim.Update(QueueResidueParks(id, cfg.Region, scope{Location: TrimSuffix(location, "/"), Incarnation, EvidenceAt: rec.Move.FlippedAt, RequestLimit: 120 s}, generations, newID))` once (journal step); `cfg.Park.Run(ctx, Work{Host, ID, Op})` per chore (ErrPending expected), then `cfg.Park.Parked` must be true for each before advancing. Per-generation chores are what let the acknowledgement finish after the probe makes the new generation live in this location (D6; a whole-location park would meet `remote.go:346`'s live-set refusal forever); the acknowledgement half is C4's runner. |
| - `building`: `moves.BuildGeneration` with the seal from the claim journal (`DecodeSeal(obs.Claim.Op.Journal[JournalSeal])`), `ReplicaPrefix` in the own bucket, `GenerationPrefix(holder.location, id)`, `WorkDir = JournalDir`. |
| - `published`: journal only (no `generation.json`, R10-3). |
| - - `local`, in this order (round-2 finding 8; `park/local.go:202` requires paths **relative to `DataDir`**, tags them with the captured stamp and refuses a path already tagged with another stamp): (1) `captured, err := hostguard.Capture(dataDir, host)` (now `F`); (2) when the journal's `previous` names `S'` and `Inventory[cfg.Box]` has paths tagged `S'`: `cfg.Local.RetireArtifacts(ctx, host, captured, <the park-local chore id>, id, S')` parks every artifact tagged `S'` (the old `<host>.db`, `<host>.db.gen`, `<host>/`, `<host>.env`) into `.parked/<inc>/<host>/<retirement>/artifacts/`, which clears their inventory entries; when there is no `S'`, `AcknowledgeEmpty`; the test asserts on the filesystem that the old files are gone from the live paths and present under `.parked/`, not on the call returning nil; (3) `archive.Open` + `Extract` the newest archive from `FilesPrefix` (by the seal's files version) into `<DataDir>/<host>/` and `<DataDir>/<host>.env`; (4) `cfg.Local.RecordInventory(ctx, host, captured, "flat", []string{host + ".db", host + ".db.gen", host, host + ".env"}, check)` registers the new installation under `F`; (5) `ReplaceTimers(host, snapshot.Rows)` from the sealed timers version (through `SelectedTimers.ReplaceTimers` once C3 lands; `registry.ReplaceTimers` directly behind the switch until then); `SetReplication(host, Source.Replication)`, `SetNextDue(host, Source.NextDue)`; `RemoveVerifyEntry`; (6) the **prepared files manifest** of spec rev 7.21 §7.6 (L17, R18-1; claim rev 6.30 73b1b9dc) under the installation's authority, never the session-bound publisher (no target session exists yet): `op.exec` writes the installed files, `<host>.env` and the wake state as create-only content objects under `<location>files/<generation>/objects/<seq>-<sha256>` and then the one create-only manifest `<location>files/<generation>/manifest.json`, bound to `{location, generation, op id, installation stamp}`, naming exact versions, at sequence 0, never replaced and retired with its generation, through C8's prepared-manifest build (region HA :809; `files.BuildPrepared(ctx, store, location, generation, opID string, stamp uint64, dir, envPath string, wake WakeState) (manifestVersion string, err error)` or its real name), journalled like the install manifest; a replay reuses the objects by digest and the manifest by key; the session-bound publisher starts at the probe's admission (Plan 3 Task 5), takes this manifest by exact version as its baseline and publishes from sequence 1 (a journal step, skipped with a log line until C8 exists); (7) fsync the directory; write `.files-gen` `{"op": id, "incarnation": inc}` last. |
| + - `local`, in this order (round-2 finding 8; `park/local.go:202` requires paths **relative to `DataDir`**, tags them with the captured stamp and refuses a path already tagged with another stamp): (1) `captured, err := hostguard.Capture(dataDir, host)` (now `F`); (2) when the journal's `previous` names `S'` and `Inventory[cfg.Box]` has paths tagged `S'`: `cfg.Local.RetireArtifacts(ctx, host, captured, <the park-local chore id>, id, S')` parks every artifact tagged `S'` (the old `<host>.db`, `<host>.db.gen`, `<host>/`, `<host>.env`) into `.parked/<inc>/<host>/<retirement>/artifacts/`, which clears their inventory entries; when there is no `S'`, `AcknowledgeEmpty`; the test asserts on the filesystem that the old files are gone from the live paths and present under `.parked/`, not on the call returning nil; (3) `archive.Open` + `Extract` the newest archive from `FilesPrefix` (by the seal's files version) into `<DataDir>/<host>/` and `<DataDir>/<host>.env`; (4) `cfg.Local.RecordInventory(ctx, host, captured, "flat", []string{host + ".db", host + ".db.gen", host, host + ".env"}, check)` registers the new installation under `F`; (5) `ReplaceTimers(host, snapshot.Rows)` from the sealed timers version (through `SelectedTimers.ReplaceTimers` once C3 lands; `registry.ReplaceTimers` directly behind the switch until then); `SetReplication(host, Source.Replication)`, `SetNextDue(host, Source.NextDue)`; `RemoveVerifyEntry`; (6) the **prepared files manifest** of spec rev 7.21 §7.6 (L17, R18-1; claim rev 6.30 73b1b9dc) under the installation's authority, never the session-bound publisher (no target session exists yet): `op.exec` writes the installed files, `<host>.env` and the wake state as create-only content objects under `<location>files/<P>/objects/<seq>-<sha256>` and then the one create-only manifest `<location>files/<P>/manifest.json`, bound to `{location, P, op id, installation stamp}`, where **`P` is the installation's prepared destination generation, `"g-" + src.GenerationOp`** (the move id here; the rollback id for Plan 4's dirty return, `installSource.GenerationOp`), **never `holder.generation`**, which still names the source's pre-move generation until the probe's admission (spec rev 7.22 §7.6, R19-1: a manifest bound to or filed under the source's generation could never be the new generation's baseline, and the first landing admission would block); `TestLocalBindsThePreparedManifestToTheNewGeneration` asserts the manifest key is `files/g-<op>/manifest.json` and its binding names `g-<op>` while the claim's `holder.generation` is still the source's; naming exact versions, at sequence 0, never replaced and retired with its generation, through C8's prepared-manifest build (region HA :809; `files.BuildPrepared(ctx, store, location, generation, opID string, stamp uint64, dir, envPath string, wake WakeState) (manifestVersion string, err error)` or its real name, called with `generation = P`), journalled like the install manifest; a replay reuses the objects by digest and the manifest by key; the session-bound publisher starts at the probe's admission (Plan 3 Task 5), takes this manifest by exact version as its baseline and publishes from sequence 1 (a journal step, skipped with a log line until C8 exists); (7) fsync the directory; write `.files-gen` `{"op": id, "incarnation": inc}` last. |
| - `configured`: `RenderHeld`; `MaterializeHeld(rt, rec.Move.Inputs.Release)`; the ack's `BundleETags` must equal `rec.Move.Inputs.BundleETags` and its overlay etag `Inputs.OverlayETag`, else `MoveRollBack` (Plan 4 Task 1's transition; until it exists, write `Reason` and stop: `markForRollback(rec, "configuration changed during the move")` is the one call site Plan 4 replaces). |
| - `restored`: copy the built database to the row's `DBPath` (flat), sha256 == manifest's, `lease.CheckDB`, write `<dbPath>.gen` `{"op": id, "incarnation": inc, "location": holder.location}`, `MoveLandingPhase(configured → restored, stamp Landing.InstalledDBSHA256)`. |
| - `landing_deadline` passed before `restored` → the rollback marker as above. |