Commit Graph
161 Commits
Author SHA1 Message Date
kyle 76263ccbf0 docs: AGENTS.md — bus crate layout, standalone gui, decision C in core constraints
CI / msrv (push) Successful in 1m5s
CI / ci (push) Successful in 2m55s
2026-10-25 17:21:31 -07:00
kyle e40f0d5052 fix(gui): cap ipc_client frame reads at the service's 1 MiB frame cap
A torn stream could yield an arbitrary u32 length prefix, turning a
re-dial into a multi-GiB allocation attempt. Mirror the service reader:
len > MAX_FRAME is InvalidData, so the worker treats it as a dead
connection and re-dials instead of OOMing.
2026-10-25 01:53:28 -07:00
kyle 6c6729324e docs: fleet console, bus transport, widened IPC 2026-10-25 01:24:38 -07:00
kyle 2a917088f7 feat(gui): node window (status strip, observe, belief, store, events, container) 2026-10-25 01:05:37 -07:00
kyle 97ea5f8f12 feat(gui): launcher window (create fleet, node controls, actions) 2026-10-25 00:36:41 -07:00
kyle 2be008e4b0 feat(gui): docker orchestration and fleet supervision (bus daemon) 2026-10-25 00:08:57 -07:00
kyle 01cd416bf0 chore(gui): ignore the standalone crate's target dir 2026-10-24 23:56:48 -07:00
kyle ca4ec4c5dd feat(gui): tines-gui skeleton (ipc client, config gen, theme) 2026-10-24 23:56:43 -07:00
kyle 7b30550454 feat(docker): runtime image for the tines service 2026-10-24 23:16:07 -07:00
kyle e275967e6f feat(host): service transport choice (bt|bus) and lib-level config 2026-10-24 23:13:31 -07:00
kyle ee87808b05 feat(bus): tines-bus transport and daemon over a host IPC bus 2026-10-24 23:00:37 -07:00
kyle 01373d809d feat(host): widen service IPC — addressed observe, List, contact/drop events
New public names: ContactOut, DropOut, ObservationOut, LIST_CAP.
2026-10-24 22:36:55 -07:00
kyle c1cad1661c feat(core): emit Contact/Lost NodeEvents (decision C) 2026-10-24 22:28:08 -07:00
kyle 0a8eda3077 docs(superpowers): fleet console design doc and implementation plan 2026-10-24 22:23:17 -07:00
kyleandClaude Opus 5.5 a4be737f17 ci: pin cargo-semver-checks by version and checksum
CI / msrv (push) Successful in 52s
CI / ci (push) Successful in 1m57s
Download the v0.51.0 release binary directly and verify its SHA-256 instead
of installing whatever cargo-binstall and cargo-semver-checks are latest,
matching the SHA-pinned actions elsewhere in the workflows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 11:44:20 -07:00
kyle 7f66884ab5 update tines-ros2 keywords 2026-10-22 10:20:30 -07:00
kyle 2a4cc82336 update tines keywords 2026-10-22 10:20:03 -07:00
kyle 09ca4e19e1 ci: gate tines-core 1.x changes against the published 1.0.0
CI / msrv (push) Successful in 53s
CI / ci (push) Failing after 13m24s
cargo-semver-checks with a pinned 1.0.0 baseline in scripts/ci.sh (skipped
where the tool is absent); the GitHub Actions ci job installs a prebuilt
binary so the gate is enforced there.
2026-10-22 09:19:47 -07:00
kyle c973657648 docs(changelog): mark tines-core 1.0.0 released
CI / msrv (push) Successful in 54s
CI / ci (push) Successful in 2m1s
v1.0.0
2026-10-22 09:16:04 -07:00
kyle 09694e36de test(fuzz): spend the 24 CPU-hour release budget
CI / msrv (push) Successful in 1m11s
CI / ci (push) Successful in 2m13s
12 workers x 7202s (~24 CPU-hours), both modes via the input-selected mode
byte. Zero crashes. Corpus regrown and minimised: 20,522 units / 81MB
(post-merge). Final worker: 513,355 runs, 71 exec/s, 1,606 new units.
2026-10-22 08:44:10 -07:00
kyleandClaude Opus 5.5 a85f57a869 docs(core,hosts): crate-level Stability section; fix comments that contradict the code
- tines-core's crate docs carry the spec §8 compatibility policy, as the
  README does.
- tines-bt: the module and BluerRadio::new docs claimed an optional or
  empty allowlist. The allowlist is exactly the configured peer (F3).
- tines: the poll loop called Store::flush "infallible". It returns a
  Result, and a failure is logged and retried on the next schedule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 0cb1ad5360 docs: sweep README, CHANGELOG, THREAT_MODEL and AGENTS after the 1.0 review fixes
- CHANGELOG: tines-core 1.0.0 and the hosts' 0.4.0 are separate,
  unreleased sections. Every core API break is listed (the nine
  pub(crate) items, the private modules and the frame_sender path, the
  local constants per decision K, non_exhaustive types, store_mut). The
  new names (WallClockStore, Node::set_wall_clock, peer_time_estimate,
  flush, IdentityChanged) are called out. The host section covers
  tines::trust, the config changes, decision J, snapshots, and the
  review fixes.
- THREAT_MODEL: F6, F8, F9 and F13 are fixed with their commits. F18 is
  partly fixed: the store side and the flush on first trusted time are
  done, but timed and shutdown flushes are still open. F19 now records
  decision J and the plausibility bounds, and what remains. §9, §10 and
  §11 are updated to match.
- README: Dest and sig in the observation table; flush and is_empty in
  the store table; only the public wire constants are listed; a new
  "Trusted time" section with the config keys and the systemd ordering;
  the MSRV and fuzz CI jobs; frame_sender's new path.
- AGENTS: 1.0 is implemented but not yet published. The constants list
  shows the pub(crate) split, PEER_IDLE_MS, MIN_MTU's role and where
  MAX_FRAGMENTS lives. Notes the hidden conformance module.
- tines-bt README: frame_sender's path, and its own MAX_PEERS.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 a0dcc0ad89 test(fuzz): measure the recovery probe from the latest tick
Fuzz steps can move the clock backwards, while a peer's idle time counts
from its last frame, so advancing from the current tick could stay short
of the idle window. Track the latest tick and probe from it; skip the
probe when the tick is too close to u64::MAX to advance. A 5-minute
nightly run (84k inputs) is clean after the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 40ae18e054 test(fuzz): un-ignore the peer-table recovery probes now that S1 is in
Also match DropReason::IdentityChanged as a unit variant in the harness,
since the variant-level non_exhaustive is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 0fc6333c17 test(fuzz): make the harness check what spec §7 says it checks
The fuzz oracle and its proptest twin checked much less than §7 claims
(S8). The harness now:

- records every frame the node attempts to send, before the transport
  can refuse it, so the "every frame fits mtu()" check can fail;
- reads the MTU from its own u16 (input bytes 1-2), mapped into
  MIN_MTU..=MAX_FRAME, instead of reusing the first step tag;
- reconciles drained drop counts per address: VersionMismatch/Malformed
  drops equal the frames that fail decode (the overflow slot makes up
  any difference), IdentityChanged drops never exceed frames whose
  `from` changed, and nothing is counted beyond the frames received;
- checks no addressed observation is stored, or forwarded in an Obs
  frame, past its lifetime;
- checks signatures round-trip from the input (an observation stored from
  a whole Obs frame keeps exactly its signature, and a stored signature
  never changes), replacing the input-independent sig_round_trip call;
- checks every acked id is stored (F1) and bounds the broadcast ids
  offered per received summary by SEND_QUEUE_CAP (F9);
- has a real adversarial Mode (input byte 0): well-formed Obs, Frag,
  Summary and Ack frames from 3 x MAX_PEERS addresses, with signed and
  max-size observations (u16 payload lengths), chosen observers
  (including the node's own id), ages, lifetimes and addressing,
  structured fragment streams that conflict, repeat or arrive out of
  order, replayed summaries, `from` values that change within a contact,
  and Acks from the peer's real id naming ids the node really sent;
- adds the §10 peer-table recovery probe, which the fuzz target runs
  after every adversarial input.

fuzz_twin.rs now runs the fuzz entry point on random bytes, generated
adversarial steps, and two rewritten properties that were trivially
true: summary_repeat_does_not_resend seeds broadcasts and checks a
replay re-sends nothing while the next on-time summary re-sends all;
peer_table_recovers_after_flood floods the table, checks it is full,
and probes recovery after PEER_IDLE_MS. The recovery tests are
#[ignore]d until the S1 idle sweep lands, and the fuzz target fails on
the first adversarial input until then. A 180 s nightly run with the
probe disabled found nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 4206e3f628 build(core): leave the fuzz twin out of the published package
tests/fuzz_twin.rs includes ../../fuzz/src/lib.rs, which the tines-core
package doesn't ship, so the published source carried a test that can't
build (S13, packaging). `cargo package --list` now omits it and the
packaged crate verifies; tests/vectors.rs still ships, since src/lib.rs
includes it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 749d6f54ce ci(fuzz): run cargo-fuzz on nightly and keep crash reproducers
rust-toolchain.toml overrides `rustup default nightly`, so the scheduled
job ran cargo-fuzz on stable and its -Z flags failed (B7). Every cargo
call now names +nightly. Also:

- upload fuzz/artifacts/node/ when the run fails;
- drop the false claim that the corpus "grows in place across
  schedules": new inputs live only in a 7-day artifact until someone
  minimises and commits them;
- pin actions by SHA as ci.yml does (checkout v4.4.0, upload-artifact
  v4.6.2), and install cargo-fuzz with --locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 7a31edd6ad ci: build tines-core on MSRV instead of testing it
The msrv job ran `cargo test` on 1.82, which can't parse a proptest
dev-dependency that needs edition 2024 (B5). The MSRV promise covers
building tines-core, so the job now builds it on 1.82 with default
features, with --no-default-features, and for thumbv7em-none-eabihf.
`cargo +1.82.0` makes the toolchain explicit over rust-toolchain.toml.
All three builds pass locally on 1.82.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 e65a9dee6c build(fuzz): require tines-core 1 and check the fuzz crate in ci.sh
fuzz/Cargo.toml still required tines-core 0.3.0, so since the 1.0.0
version bump `cargo fuzz run node` failed version selection (B6). The
requirement is now "1" and the lock is regenerated.

scripts/ci.sh now runs rustfmt and clippy (all targets, -D warnings) on
the out-of-workspace fuzz crate on the pinned stable toolchain, so it
can't silently stop building again. fuzz/.gitignore ignores target,
artifacts and coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 13aaa5d5f7 test(sim): restore 64 proptest cases; check the bounce table overflows
60337d6 cut the shared props config from 64 to 16 cases for every
property to keep bounce_overflow_keeps_a_holder cheap (S12). The shared
block is back at 64; the bounce-overflow property gets its own 16-case
block, and now asserts that more than SEND_QUEUE_CAP ids left node 0
within one contact, so the table really overflowed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 38b97577a4 test(core): probe the agreed 1.0 public surface
public_api.rs imported only spec §3's original list (S15). It now also
probes WallClockStore, Node::set_wall_clock, Node::peer_time_estimate and
Node::flush (decision H), the Node method signatures, the NodeEvent shape
and the public wire constants, and names nothing §3 makes private
(Frame, WireError, local constants).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 18e53b9bdd refactor(core): make local constants crate-private
Decision K (spec §5): the local capacities and timings are defaults that
will become configuration, so they are `pub(crate)`. The wire constants
stay public.

- MAX_PEERS, SUMMARY_INTERVAL_MS, REASSEMBLY_TIMEOUT_MS, SEND_QUEUE_CAP,
  MIN_MTU, EVENT_QUEUE_CAP, DROP_TABLE_CAP, BCAST_SENT_CAP, ACK_BUFFER_CAP
  and PEER_IDLE_MS are now `pub(crate)`; a compile_fail doctest pins it.
- MIN_MTU's doc claimed it was the floor below which a fragment has no
  room. It is only the frag sweep's lower bound (BLE ATT's 23). The real
  floors are now stated on `Transport::mtu` and pinned by
  `consts::tests::mtu_floors`: 85 to carry every valid observation, 66 to
  fragment at all, 35 for an Ack, 26 for a summary.
- tines-bt caps live connections with its own MAX_PEERS (8).
- tines-sim's props mirror MAX_PEERS/SEND_QUEUE_CAP, checked against core
  through the hidden `conformance` module; the fuzz twin reads it there.
- node.rs: drain_events' rustdoc no longer links the now-private consts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 e2717a930d refactor(core): make Frame and WireError crate-private
Spec §3 (decision D-b) lists `Frame` and `WireError` as private; lib.rs
re-exported both. Only `frame_sender` stays public.

- The pinned vectors in tests/vectors.rs now compile in-crate as a
  `#[cfg(test)] #[path]` module (autotests = false keeps Cargo from also
  building them as an integration test). Every golden vector now checks
  `encode() == GOLDEN` as well as the decode, and new literal vectors pin a
  trailing-byte rejection and an addressed Obs with a nonzero age and an
  `observed_at`.
- A `#[doc(hidden)]` `conformance` module (behind the `conformance`
  feature) gives the fuzz harness byte-level frame encoders/decoders and
  core's local limits without exposing `Frame`. The fuzz_twin test target
  now requires `conformance`, which scripts/ci.sh enables.
- compile_fail doctests pin that `Frame`/`WireError` are unreachable.
- Remove the THROWAWAY gen_missing_golden_vectors test and fix stale
  wire.rs comments about the version/tag byte offsets.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 d91d7914fc fix(hosts): pass the plausible wall range to the store
Both hosts open LogStore with plausible_wall_ms(PLAUSIBLE_WALL_MS), so a
forged out-of-range peer estimate is dropped before the store picks the
latest and can't hide an honest one (decision J, review B3). Un-ignores
latest_estimate_wins_over_smallest_age now that the store returns the
latest estimate, and adds forged_estimate_does_not_hide_an_honest_one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 15bbdbd4a0 fix(store): peer estimates outside a plausible wall range don't count
Spec §9 decision J: an estimate outside the plausibility bounds doesn't
count. A max-based peer_time_estimate always returned a forged
observed_at = u64::MAX, which the host then rejected, so it never saw
the honest estimates and stayed quarantined.

LogStoreBuilder::plausible_wall_ms(floor..=ceiling) sets the range
(default unbounded, so behaviour is unchanged when unset), and
peer_time_estimate filters candidates against it before taking the
max. The WallClockStore signature is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 01db6c0b71 docs(core): rewrite flush, WallClockStore and age_ms rustdoc from the spec
- Store::flush and Node::flush: the §4 contract (durability point, time
  anchor, harmless twice, earlier now clamped, Err(Backend) leaves the
  store usable, application decides when). No "infallible" or
  "duplicate custodian" wording.
- WallClockStore and Node::set_wall_clock / peer_time_estimate: define
  quarantine generically, a second set_wall_clock is a no-op returning
  the first report, peer time is unauthenticated until 1.1.
- Observation::age_ms: a lower bound that every holder advances; use
  observed_at for absolute time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 649b2a8900 test(core): make the conformance flush case check the §4 contract
idempotent_flush now also checks that an earlier `now` is clamped (no
age moves), that a flush never ages an observation inserted after its
`now`, that a release survives a flush, and that the store accepts
inserts afterwards. Durability itself needs a restart, which the
std-free suite can't do; tines-store tests it through the trait.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 382254faaf fix(store): snapshot the log after each fold; flush through the trait
Review findings B1, B2, S2, S3, B3 (store sites), decision I, J and L.

- B1: the clock-marker flush is now the `Store::flush` impl, so
  `Node::flush` writes the marker and fsyncs. The inherent
  `LogStore::flush` is gone (calls resolve to the trait method).
- B2 / decision I: `open` (after the tick fold) and `set_wall_clock`
  (after the wall fold) rewrite the log as a snapshot: header, Boot,
  every live Obs at its current age with stored_at = now, a Release
  for every released id still held, and a closing Clock marker (no
  wall at open, wall_now after the fold). Temp file beside the log,
  fsync, rename, fsync dir. Every stored_at on disk is then in the
  current boot's ticks, so lineage survives any number of reboots, and
  an expiry or floor from the fold survives a later reboot. A leftover
  temp file from an interrupted snapshot is removed on open.
- Lock across the rename: the snapshot's temp file is created 0600 and
  locked before it is renamed over the log, so the file at the path is
  always locked by the live store. An opener that locked a file a
  snapshot just replaced detects it (dev/inode check) and retries.
- S2: the known-downtime fold adds downtime minus the ticks the open
  fold counted minus (now - open_tick), once, not per-entry
  now - stored_at, so a flush before trust no longer double-counts.
- S3: flush and set_wall_clock clamp `now` to the time anchor (the
  latest tick the store has seen); stored_at never moves backwards.
- B3: the derived marker wall and the expiry `lifetime + 1` saturate.
- Decision J: peer_time_estimate returns the latest estimate among
  non-quarantined observations with observed_at; rustdoc says peer
  time is unauthenticated.
- Decision L: trim rustdoc states quarantined entries are exempt from
  every rule; tested.
- A header torn while the log was first created (shorter than the
  header, starting with the magic) opens as a new log; any other short
  file is still refused and left alone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 8c97963364 chore: bump hosts to 0.4.0
The host crates ship breaking changes (log format v2, flush and
set_wall_clock, the tines::serve signature, tines-core 1) that can't go
out as the already-released 0.3.0 (1.0 review S13). tines-core stays
1.0.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 3db1e97545 feat(ros2): local trusted time and decision J's peer-time window
- S4: TinesNode::trust_system_clock(now) and trust_wall_clock(wall, now)
  give a ROS2-only fleet a local trusted-time source, with the same
  plausibility bounds as the service (tines::trust).
- Decision J: poll collects peer estimates for a window (default 10 s,
  set_peer_time_window_ms) from the first plausible one and adopts the
  latest, once; implausible estimates (B3) are never adopted.
- S5: adoption goes through tines::trust::adopt_wall_clock, which flushes
  the store right after set_wall_clock.
- has_trusted_time() reports whether a wall time was adopted.

The `ros2` feature is outside the default gate; the shared logic is
tested in `tines`, and these methods were type-checked against it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 0b4b5d1b59 fix(tines): bounded, windowed trusted time for the service
- B3: HostClock::wall_ms saturates. New `tines::trust` module: wall times
  outside WALL_FLOOR_MS (2026-01-01Z) ..= WALL_CEILING_MS (+100 years) are
  never adopted, from a peer or the system clock.
- Decision J: `serve` takes `peer_time_window_ms`; PeerTimeWindow opens on
  the first plausible estimate, keeps the latest (advanced by ticks), and
  adopts once when the window closes. The scan stops once trusted.
- S5: adopt_wall_clock flushes right after set_wall_clock.
- S6: `trusted_clock` accepts only "system", Config denies unknown fields,
  an implausible system clock is logged and left to the peer path, and the
  docs say to order the unit after time-sync.target. New config key
  `peer_time_window_ms` (default 10000).
- Tests: the observed_at and time_from_peer tests now run the host's own
  logic through `serve`; smallest_age_wins is replaced by window and
  latest-estimate tests (the latter ignored until the store returns the
  latest estimate); forged u64::MAX and below-floor estimates aren't
  adopted and don't panic the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 97a97379e2 fix(core): free drop-table keys on drain; summary rate-limit slack
- The drop table now empties on drain, keys included. Before, after 32
  distinct (addr, reason) pairs every later drop went to the overflow
  slot until reboot. The overflow slot's reason is documented as that of
  the first drop to overflow since the last drain (review S7).
- F9's receive-side check accepts a summary at least
  SUMMARY_INTERVAL_MS / 2 after the last one acted on, so an on-time
  summary measured at 1999 ms isn't ignored (decision L, review D-f).
- DropReason::IdentityChanged loses its variant-level non_exhaustive,
  which made it unmatchable outside the crate; the enum keeps its own.
- Stale comments: drain_events is used by Node (drop the dead_code
  allow), and NodeEvent drops are this node's, not the peer's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 d983fea024 fix(core): reassembled observations must decode canonically
The reassembler decoded with postcard::from_bytes, which accepts
trailing bytes. It now uses take_from_bytes and rejects a non-empty
remainder, as Frame::decode does (spec §10 F13).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 eda9746d8e fix(core): evict silent and contact-only peers (F8)
The idle check ran only in on_frame and only for the peer that just
sent, so a peer that went quiet, or never sent (contact-only), held its
slot forever: eight spoofed addresses locked out every newcomer
(review S1).

Peers now record when their contact started (added_at), and idle peers
(now - last_inbound.unwrap_or(added_at) > PEER_IDLE_MS) are swept as if
Lost before a peer is added and at the top of next_frame. A peer that
resumes after its idle window is an implicit new contact: its frame is
processed rather than silently dropped. A repeat Contact restarts the
idle clock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 ac2bf37719 fix(core): scope acks, receipts and the F6 pin to the contact
- remove_peer purges only the removed peer's pending acks, not every
  peer's (review S9, F13).
- bcast_sent records for an address are dropped when its contact starts
  or ends, and a broadcast whose send fails has its record undone, so a
  broadcast Ack is accepted only for this contact's dispatched frames
  (review S10).
- A repeat Contact for a known peer starts a new contact: it resets the
  F6 pin and the per-contact bounce and receipt records (spec §10 F6).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 caa245dda4 fix(core): never trust store filtering for summaries and broadcast diff
Core re-checks dest when building the summary Bloom filter, rebuilding
a peer's broadcast send queue, and emitting a broadcast, so a store
whose for_each_broadcast_id yields addressed ids can't leak them into a
summary or forward them (expired) as broadcasts (review S11).

Adds UnfilteredStore, a test store whose iterators yield every held id,
released and broadcast ones included, pinning spec §4's 'core never
trusts a backend's filtering' for custody, acks and summaries (B8).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 5a7722bf25 docs(spec): decisions I–L from the 1.0 review
I: LogStore snapshots the log at the end of open and after the
set_wall_clock fold, so fold results and age lineage survive reboots.
J: hosts adopt the latest peer time estimate within a short window,
bounded, once per run.
K: local constants become pub(crate); only wire constants stay public.
L: quarantined entries are exempt from the count trims; the F9 summary
rate limit accepts at SUMMARY_INTERVAL_MS / 2; Observation.sig stays a
public field and §2's wording is corrected.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-22 08:44:09 -07:00
kyleandClaude Opus 5.5 4f7fe8f2c2 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>
2026-10-22 08:44:09 -07:00
kyle 8425232ba1 release(core): tines-core 1.0.0 2026-10-22 08:44:09 -07:00
kyle 164cf97a5b test(core): fuzz harness and proptest twin 2026-10-22 08:44:09 -07:00