carlos / platform Public

plans: install.json carries claim Plan 6's snake_case keys (v, op, host, n or snapshot_txid, snapshot_*, anchor_*, integrity_check, db_sha256, built_at); Plan 2 Task 8 expects and tests them; spec §7.6 to name them at the round-19 touch

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 0a4cadd1028a698910e215944109d547b998d944 parent b78b327
2 files changed, +2 −2
  • docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md +1 −1
  • docs/superpowers/plans/2026-10-06-moves-phase1-2-cut-flip-landing.md +1 −1

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md b/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md
index f7e5bec6b..801e8e26c 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-0-index.md
@@ -144,7 +144,7 @@ Claim Plan 6 Task 6 on `feat/hostclaim-box` (bf63e250) implements C1 to C4 and d
| type | claim Plan 6 file | our definition | shape check |
|---|---|---|---|
-| `moves.InstallManifest` | `internal/moves/install_contract.go` | Plan 2 Task 8 | identical: `V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time`, no json tags on either side |
+| `moves.InstallManifest` | `internal/moves/install_contract.go` | Plan 2 Task 8 | identical: `V int; Op, Host string; N uint64; SnapshotKey, SnapshotVersion string; SnapshotCRC64 uint64; AnchorKey, AnchorVersion string; AnchorCRC64 uint64; IntegrityCheck string; DBSHA256 string; BuiltAt time.Time`; platform-1e is adding snake_case json tags before merging (`v`, `op`, `host`, `n` or `snapshot_txid`, `snapshot_key`, `snapshot_version`, `snapshot_crc64`, `anchor_key`, `anchor_version`, `anchor_crc64`, `integrity_check`, `db_sha256`, `built_at`); Plan 2 Task 8 expects those keys and tests them; spec §7.6 names them at its next revision (round 19) |
| `moves.ErrAttemptOver` | `internal/moves/install_contract.go` | Plan 3 Task 1 | identity only (`errors.Is`); Plan 3 now uses claim Plan 6's message text |
| `provision.MoveInputs` | `internal/provision/moveinputs_contract.go` | **Plan 1 Task 2** (the record journal; it lands with the first task of the series, so our side may define it first) | identical fields and json tags: `bundle_etags`, `overlay_etag`, `release`, `files_plain`, `env_report`, all `omitempty` |
diff --git 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
index ed6ae31e0..060884d1b 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, no json tags, in `internal/moves/install_contract.go`, beside `moves.ErrAttemptOver`. 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**; `install.json` is then encoded with Go's default field names, as the contract has no tags, and this task adds none); `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`, 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.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).