rastrillo / native Public

Spec: replace refuses in-flight ids; compact joins the property alphabet

Addresses the delta review on #5. replace throws on any in-flight id,
requires a seq-contiguous run within one lane, and compaction runs only
over sendable, not-in-flight entries, otherwise an ack could delete
never-sent content. The property test gains compact (overlapping an
in-flight batch) and a crash between replace steps 1 and 2. Also: set
isExcludedFromBackup on the account directory and pass
.completeFileProtection with the write; M6 holds even if K5 ships after K3.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by paul@keymail.dev 604983003308c3ff6e91f8dd4bf94e1a3565f1d3 parent 0d5c0c4
1 file changed, +22 −5
  • docs/superpowers/specs/2026-09-23-offline-kit.md +22 −5

CI failed — run details

diff --git a/docs/superpowers/specs/2026-09-23-offline-kit.md b/docs/superpowers/specs/2026-09-23-offline-kit.md
index 338e54b..7d24585 100644
--- a/docs/superpowers/specs/2026-09-23-offline-kit.md
+++ b/docs/superpowers/specs/2026-09-23-offline-kit.md
@@ -218,7 +218,9 @@ Application Support/<bundle id>/accounts/<account.id>/
```
- Everything is written by `NativeSealedStore` (§3.6). On iOS the file
- protection is `.complete`, and files are `isExcludedFromBackup`: the key
+ protection is `.complete`, passed as `.completeFileProtection` in the same
+ `write` options. `isExcludedFromBackup` is set on the `accounts/<id>/`
+ directory, not per file, because atomic writes create new inodes. The key
cannot leave the device, so a backed-up copy could never be read anyway.
- **"Durable on return" means it survives a process kill** *(L2)*: the file is
written with `Data.write(options: .atomic)`, a temporary file renamed into
@@ -307,7 +309,17 @@ exercise every method that ships, which is the "one builds, one adopts" bar:
2. delete the other replaced entries.
A crash after step 1 leaves duplicates only, never a gap and never
- reordering. Only a lane whose payloads tolerate duplicates may use
+ reordering.
+
+ **`replace` throws if any id is in flight**, exactly as `update` does
+ (§5). The ids must be a seq-contiguous run within one lane. The merged
+ entry gets a new UUID and inherits the lane and kind. Compaction runs
+ only over sendable entries that are not in flight. Without this rule, a
+ byte-cut batch in flight across a compaction would, on its `ack`, delete
+ the merged `<oldest seq>.entry`, and with it the content of entries
+ whose own files step 2 had already removed.
+
+ Only a lane whose payloads tolerate duplicates may use
`replace`. Docs qualifies (Yjs integrates duplicates idempotently: docs
`internal/docs/handlers.go:879-880`).
- **No silent caps.** An app sets a per-lane byte ceiling. Exceeding it is a
@@ -450,8 +462,11 @@ it.
- **Accounts without a subject** *(M6)*. "Accept the first subject seen"
would disarm the guard for every account that exists today. Instead,
the first launch of a build that carries the kit marks every subject-less
- account session-ended. That happens before any outbox exists, so nothing
- is at risk, and the one re-sign-in records the subject. Apps hold no real
+ account session-ended. When K5 ships in the same build as K3, that
+ happens before any outbox exists. If it doesn't, subject-less accounts
+ are still marked session-ended, and the outbox survives that state, so
+ nothing is at risk either way. The one re-sign-in records the subject.
+ Apps hold no real
data and Paul is the only user, so one sign-in per app is the whole cost.
(A `/native/whoami` endpoint read on the first online request would avoid
that sign-in. It is not proposed, because it is another endpoint for a
@@ -621,7 +636,9 @@ there. `RastrilloTestSupport` (§3.1) ships four primitives:
**The Docs property test (K3).**
- **Event alphabet:** edit; writer tick, *including between `command` and
- `enqueue`*; ack; lose-response; crash at any store operation; reopen and
+ `enqueue`*; ack; lose-response; **compact**, sometimes while a batch
+ overlapping the candidates is in flight, which must throw; crash at any
+ store operation, including between `replace` steps 1 and 2; reopen and
apply the lane; reconnect.
- **Assertions, after every event and at the end:**
- I1 (§4.3);