docs(spec): decision H — WallClockStore and the Node wall-clock accessors

Record the public 1.0 API that 941a7ce introduced in place of the revived
store_mut backdoor: the opt-in WallClockStore trait (FoldReport,
set_wall_clock, peer_time_estimate) and Node::set_wall_clock /
Node::peer_time_estimate, bounded on S: WallClockStore. Updates §3's
public table and Node method list and §9's fold and time-from-peers
text; core still never interprets wall time.

Also split the drain_events sentence into its own §3 bullet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-22 08:44:09 -07:00
co-authored by Claude Opus 5.5
parent 8425232ba1
commit 4f7fe8f2c2
@@ -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 definition (2026-10-19); review fixes applied. v6 added §10 (core hardening
from `THREAT_MODEL.md`) and the adversarial fuzz mode; Kyle reviewed and 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; 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`, 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) `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<A>` (non-exhaustive enum, generic over the transport `Addr`; replaces `ConfidenceEvent`), `DropReason` | `Delivered { peer, id }`, `Dropped { addr: Option<A>, reason, count }` (`addr` is `None` only for the overflow counter, §1); room for more | | `NodeEvent<A>` (non-exhaustive enum, generic over the transport `Addr`; replaces `ConfidenceEvent`), `DropReason` | `Delivered { peer, id }`, `Dropped { addr: Option<A>, 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 | | `Observation`, `Dest`, `Signature`, `SigAlgo`, `NodeId`, `ObservationId`, `Tick`, `Topic`, `Payload` | the data model |
| `Store`, `StoreError`, `TrimPolicy`, `MemStore` (`alloc`) | backends and their reference impl | | `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 | | `Transport`, `Event`, `SendError`, `FrameBuf` | transport adapters |
| `BeliefPolicy`, `LatestWins`, `SelfFirst` | the memory layer | | `BeliefPolicy`, `LatestWins`, `SelfFirst` | the memory layer |
| `consts` (wire constants + local caps, §5), `WIRE_VERSION` | sizing | | `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 - It gains `flush(now) -> Result<(), StoreError>`, which delegates to
`Store::flush` (§4; decision F). `Store::flush` (§4; decision F).
- `store_mut`, added by the fleet fixes so the service could reach - `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<T::Addr>`s: queued `LogStore::flush`, is removed. Read access through `store()` stays.
`Delivered` events, then one `Dropped` per nonzero drop counter (§1). - It gains `set_wall_clock(wall_now, now) -> S::FoldReport` and
`peer_time_estimate(now) -> Option<u64>`, which exist only when
`S: WallClockStore` and delegate to it (§9; decision H).
- `drain_events` returns `NodeEvent<T::Addr>`s: queued `Delivered`
events, then one `Dropped` per nonzero drop counter (§1).
- Every remaining public item is documented; `#![deny(missing_docs)]` goes in - 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 `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). 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 - Observations inserted after open are normal: their ages are this
boot's ticks, and they need no fold. boot's ticks, and they need no fold.
- **`LogStore::set_wall_clock(wall_now: u64, now: Tick)`** runs the 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, once, when the host first has trusted time. It isn't a `Store` trait
not a `Store` trait method: core never deals with wall time. 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 - **Downtime known:** the log's last clock marker has a wall time, and
`wall_now` ≥ it. Every quarantined observation's age gains the `wall_now` ≥ it. Every quarantined observation's age gains the
downtime: the wall time elapsed since that marker, minus the ticks 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 carries `observed_at`. The estimate is `observed_at + age_ms`, plus
local ticks elapsed since that observation arrived. Because age is a local ticks elapsed since that observation arrived. Because age is a
lower bound, this estimate is never later than the true time. The 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 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. 3. otherwise, the robot's own fail-safe applies. That's outside tines.
The quarantine simply stays until trusted time arrives. The quarantine simply stays until trusted time arrives.
- **Peer time is unauthenticated until 1.1.** A peer can claim any - **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 is floored at `unknown_downtime_age_ms` (Kyle, 2026-10-20): assume the
worst about freshness without discarding data. 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<u64>`.
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 ## Left to the builder
- The exact `WIRE_VERSION` value, within the rules in §1. - The exact `WIRE_VERSION` value, within the rules in §1.