The taxonomy table now lists the store's full rejection-code space with per- code semantics, records the transactional-offline boundary on the re-derivability property, and states the expiry contract independent of the enforcing component. The deadline's placement is motivated by what enforcement may read (the declared deadline and an arrival clock, never application payloads), matching lofi's trusted-server threat model.
- commit
- e8684e96eb776819d0b23ad7d52bdee5b6c880b5
- parent
- 3d85617558ed2d35ed18dd771e495c03b762e613
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 69 | +++++++++++++++++++----------- |
@@ -1,7 +1,7 @@
11 # Write verdicts22 3-<!-- Source: FelineStateMachine/lofi-node src/verdict.ts, tests/write_verdict_test.ts;4- jazz-tools batch-fate surface at the pinned alpha. -->3+<!-- Source: FelineStateMachine/lofi-node src/verdict.ts, tests/write_verdict_test.ts,4+ tests/write_conflict_test.ts; jazz-tools batch-fate surface at the pinned alpha. -->55 66 A lofi app treats every mutation as a durable write that eventually settles: accepted into the77 store, or rejected by it. Rejection is what compensation logic (refunds, releasing holds) hangs on,@@ -35,14 +35,19 @@ an asynchronous `rejected` verdict (the store overruled a write it had never see
3535 3636 Three properties hold, and the node's harness keeps them pinned:3737 38-1. **Correlation.** A permission-denied write surfaces `code: "permission_denied"` with the39- originating write's batch id, on both verdict surfaces.38+1. **Correlation.** A rejected write surfaces its machine-readable code with the originating write's39+ batch id, on both verdict surfaces.4040 2. **Re-derivability.** A write unacknowledged at disconnect is re-proposed on reconnect, and the4141 rejection fires then. A disconnect cannot swallow a verdict; it only defers it.4242 3. **Determinism.** Waiting again on a rejected batch re-derives the same verdict, across further4343 reconnects. An independent device issuing the same write against the same store state derives the4444 same code.4545 46+One boundary at the pinned alpha: re-derivability holds for direct writes (plain inserts, updates,47+deletes). A **transactional batch committed while offline** does not re-submit its seal on48+reconnect, so the store never adjudicates it — the write stays unapplied and waiting on it fails49+with a runtime error rather than a verdict. Transactions committed while connected settle normally.50+4651 ## At-least-once, exactly-once: the consumer's duty4752 4853 Delivery is at-least-once from the store's point of view (unacked batches are re-proposed on every@@ -65,32 +70,44 @@ the package exports (`classifyMutationError`, `MUTATION_ERROR_CLASSES`):
6570 | `permanent` | The store adjudicated the write and never accepts it | Verdict is `rejected`; compensation may run |6671 | `transient` | No adjudication happened (unreachable, timeout) | Write stays pending; keep waiting |6772 68-The pinned alpha emits exactly one rejection code over the sync protocol: `permission_denied`,69-classified permanent. The registry also carries `expired`, classified permanent; its semantics are70-the [expiry contract below](#expiry). Codes not in the registry classify as transient by design — an73+The pinned alpha's store can emit four rejection codes over the sync protocol:74+75+| Code | Store meaning | Class |76+| -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- |77+| `permission_denied` | The store adjudicated the write and refused it: policy denial under a stale client policy, content that fails the store's column validation, or schema-resolution failure. The `reason` distinguishes them; the class does not. | `permanent` |78+| `transaction_conflict` | A transactional batch staged against a row whose visible frontier moved before the store validated the seal. The store never rebases or retries the losing batch; the row converges to the winner. | `permanent` |79+| `permissions_head_missing` | The store enforces permissions but none were ever published for the app (schema deployed without a permissions head). Publishing permissions later does not revisit the fate; only new batches see the new head. | `permanent` |80+| `invalid_batch_submission` | The sealed submission itself is malformed. Not producible through the public API: every trigger is an envelope invariant the client runtime computes itself, and the one author-reachable shape (committing an empty transaction) throws synchronously before any batch exists. | unregistered, so `transient` |81+82+The registry also carries `expired`, classified permanent; its semantics are the83+[expiry contract below](#expiry). Codes not in the registry classify as transient by design — an7184 unrecognized code must never trigger irreversible compensation, because waiting is recoverable and a72-wrong `rejected` verdict is not. As the upstream code space grows (validation, schema enforcement),73-codes enter the registry here rather than in every consumer.85+wrong `rejected` verdict is not. `invalid_batch_submission` stays unregistered on the same86+principle: no rejection carrying it has been observed, so no classification has been demonstrated.87+As the upstream code space grows, codes enter the registry here rather than in every consumer.7488 7589 ## Expiry7690 7791 A write may declare a deadline: a wall-clock instant after which the app no longer wants it78-accepted. This section states the enforcement contract. **The pinned Jazz alpha does not enforce79-it** — no deadline travels on the wire today, no store rejects a late write, and the `expired` code80-is never emitted. The taxonomy entry and everything below define the behavior a store commits to81-once enforcement exists, so apps written against this page need no changes when it arrives.92+accepted. This section states the expiry contract — what a declared deadline means, how refusal93+surfaces, and when a client may safely give up — independent of which component enforces it.94+**Nothing enforces it at the pinned Jazz alpha**: no deadline travels on the wire today, no store95+rejects a late write, and the `expired` code is never emitted. The enforcement design is in progress96+at the node layer. The taxonomy entry and everything below define the behavior any enforcement97+commits to, so apps written against this page need no changes when it arrives.8298 83-### The deadline is cleartext, and client-asserted99+### The deadline is declared outside the payload, and client-asserted84100 85-Private transaction payloads are encrypted end to end; neither the sync node's access gate nor the86-store's engine can read them. A deadline the store is supposed to enforce therefore cannot live in87-the payload — it travels as an optional field on the transaction's cleartext envelope, asserted by88-the writing client as a wall-clock time. The store never inspects what the write does, only when it89-arrived.101+The deadline is asserted by the writing client as a wall-clock time and travels apart from the102+write's row data, so that enforcement never depends on interpreting application data: whichever103+component enforces expiry reads only the declared deadline and its own arrival clock, never what the104+write does. That keeps the contract intact for columns an app seals client-side, and keeps105+enforcement implementable by a component that does not evaluate application schemas at all.90106 91107 ### The enforcement rule and the skew tolerance92108 93-The store compares its own arrival time against the declared deadline and refuses the write when109+The enforcing component compares its own arrival time against the declared deadline and refuses the110+write when94111 95112 ```96113 arrival > expiresAt + tolerance@@ -120,12 +137,12 @@ what a writer can do. Guarantees against hostile writers come from
120137 121138 Client-side expiry alone is unsound: a client can never prove that a pending write is not already in122139 flight, or was not delivered before a crash, so compensating locally risks watching the write sync123-anyway. Server-side enforcement inverts this. The store accepts a write until `expiresAt` plus the124-tolerance on its own clock, so once a client — granting its own clock the same two-minute tolerance125-— observes its clock pass `expiresAt` plus twice the tolerance, future acceptance is impossible by126-construction: one tolerance for the store's acceptance window, one for the client's own possible127-offset. From that instant, retiring the write locally and running compensation is safe even for a128-device that never reconnects.140+anyway. Enforcement on the store's side of the connection inverts this. A late write is accepted141+until `expiresAt` plus the tolerance on the enforcing clock, so once a client — granting its own142+clock the same two-minute tolerance — observes its clock pass `expiresAt` plus twice the tolerance,143+future acceptance is impossible by construction: one tolerance for the acceptance window, one for144+the client's own possible offset. From that instant, retiring the write locally and running145+compensation is safe even for a device that never reconnects.129146 130147 Until enforcement exists, none of this holds: a declared deadline is ignored, an expired-looking131148 pending write may still be accepted whenever the device reconnects, and clients must keep treating