diff --git a/docs/superpowers/specs/2026-10-18-tines-1-0-design.md b/docs/superpowers/specs/2026-10-18-tines-1-0-design.md index d588ff62..7df9131a 100644 --- a/docs/superpowers/specs/2026-10-18-tines-1-0-design.md +++ b/docs/superpowers/specs/2026-10-18-tines-1-0-design.md @@ -6,7 +6,8 @@ Status: approved by Kyle (2026-10-20). Kyle decided D1–D5 and Q1–Q3 definition (2026-10-19); review fixes applied. v6 added §10 (core hardening from `THREAT_MODEL.md`) and the adversarial fuzz mode; Kyle reviewed and approved both (2026-10-20). Decisions F (`flush`) and G (age across downtime) added 2026-10-20; -G amended the same day (quarantine on open, time from peers). +G amended the same day (quarantine on open, time from peers). Decision H +(`WallClockStore`) added 2026-10-21. Builds on: `2026-10-16-tines-design.md`, `2026-10-18-tines-addressed-delivery.md`, `2026-10-18-tines-fleet-deployment-design.md` (assumed fully implemented) @@ -395,6 +396,7 @@ Public in 1.0 (everything else becomes `pub(crate)`): | `NodeEvent` (non-exhaustive enum, generic over the transport `Addr`; replaces `ConfidenceEvent`), `DropReason` | `Delivered { peer, id }`, `Dropped { addr: Option, reason, count }` (`addr` is `None` only for the overflow counter, §1); room for more | | `Observation`, `Dest`, `Signature`, `SigAlgo`, `NodeId`, `ObservationId`, `Tick`, `Topic`, `Payload` | the data model | | `Store`, `StoreError`, `TrimPolicy`, `MemStore` (`alloc`) | backends and their reference impl | +| `WallClockStore` (opt-in supertrait of `Store`: associated `FoldReport`, `set_wall_clock`, `peer_time_estimate`) | lets a host feed trusted wall time to a backend that holds data across power-off, through the `Node`, without a `&mut Store` backdoor (§9; decision H) | | `Transport`, `Event`, `SendError`, `FrameBuf` | transport adapters | | `BeliefPolicy`, `LatestWins`, `SelfFirst` | the memory layer | | `consts` (wire constants + local caps, §5), `WIRE_VERSION` | sizing | @@ -419,8 +421,12 @@ is a doc-comment link in `tines-sim/src/world.rs`, which is updated. - It gains `flush(now) -> Result<(), StoreError>`, which delegates to `Store::flush` (§4; decision F). - `store_mut`, added by the fleet fixes so the service could reach - `LogStore::flush`, is removed. Read access through `store()` stays. `drain_events` returns `NodeEvent`s: queued - `Delivered` events, then one `Dropped` per nonzero drop counter (§1). + `LogStore::flush`, is removed. Read access through `store()` stays. + - It gains `set_wall_clock(wall_now, now) -> S::FoldReport` and + `peer_time_estimate(now) -> Option`, which exist only when + `S: WallClockStore` and delegate to it (§9; decision H). + - `drain_events` returns `NodeEvent`s: queued `Delivered` + events, then one `Dropped` per nonzero drop counter (§1). - Every remaining public item is documented; `#![deny(missing_docs)]` goes in `tines-core/src/lib.rs` (26 items are missing docs as of the fleet milestone; most of them are the fields of the now-private types). @@ -636,8 +642,12 @@ the new `Observation` encoding. There is no migration: - Observations inserted after open are normal: their ages are this boot's ticks, and they need no fold. - **`LogStore::set_wall_clock(wall_now: u64, now: Tick)`** runs the fold, - once, when the host first has trusted time. It's a `LogStore` method, - not a `Store` trait method: core never deals with wall time. + once, when the host first has trusted time. It isn't a `Store` trait + method. `LogStore` implements it through the opt-in `WallClockStore` + trait, and hosts reach it through `Node::set_wall_clock` (decision H, + 2026-10-21). Core defines the trait and forwards the call, but never + reads or interprets a wall time itself. A second call is a no-op that + returns the first fold's report. - **Downtime known:** the log's last clock marker has a wall time, and `wall_now` ≥ it. Every quarantined observation's age gains the downtime: the wall time elapsed since that marker, minus the ticks @@ -674,9 +684,13 @@ the new `Observation` encoding. There is no migration: carries `observed_at`. The estimate is `observed_at + age_ms`, plus local ticks elapsed since that observation arrived. Because age is a lower bound, this estimate is never later than the true time. The - host uses the observation with the smallest `age_ms` (the least + estimate uses the observation with the smallest `age_ms` (the least accumulated error), and only one inserted after this open: anything - older is quarantined. + older is quarantined. The backend computes this estimate + (`WallClockStore::peer_time_estimate`, reached through + `Node::peer_time_estimate`), because only it knows which + observations are quarantined and when each one arrived (decision + H). 3. otherwise, the robot's own fail-safe applies. That's outside tines. The quarantine simply stays until trusted time arrives. - **Peer time is unauthenticated until 1.1.** A peer can claim any @@ -840,6 +854,31 @@ The review's other fixes are applied too: is floored at `unknown_downtime_age_ms` (Kyle, 2026-10-20): assume the worst about freshness without discarding data. +## Decisions (Kyle, 2026-10-21) + +- **H. `WallClockStore`, `Node::set_wall_clock` and + `Node::peer_time_estimate` are public 1.0 API** (§3, §9). + - During implementation, Task 10b reached `LogStore::set_wall_clock` + through a revived `Node::store_mut`. Decision F removed that backdoor, + so Kyle had it replaced with a narrow accessor (commit `941a7ce`). + This decision records that change. + - `WallClockStore: Store` is a separate, opt-in trait with: + - an associated `FoldReport` type, defined by the backend; + - `set_wall_clock(&mut self, wall_now: u64, now: Tick) -> Self::FoldReport`; + - `peer_time_estimate(&self, now: Tick) -> Option`. + + Only backends that hold data across power-off implement it. `LogStore` + does; `MemStore` doesn't. + - The two `Node` methods have an `S: WallClockStore` bound and delegate to + it, so no `&mut Store` escapes the facade. + - It's not a `Store` method, so the 1.0 trait-extension rule doesn't + apply to adding it. Any method later added to `WallClockStore` itself + falls under that rule and needs a safe default. + - Core still never reads or interprets wall time. The trait only carries + the host's value to the backend, and the backend's estimate back to the + host. This replaces §9's earlier "a `LogStore` method, not a `Store` + trait method: core never deals with wall time". + ## Left to the builder - The exact `WIRE_VERSION` value, within the rules in §1.