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>
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>
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>
- 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>
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>
- 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>
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>
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>
- 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>
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>
Task 10b reached LogStore::set_wall_clock through a general Node::store_mut.
Kyle: replace it with a narrower accessor. A WallClockStore trait (set_wall_clock
+ peer_time_estimate, implemented only by LogStore) now backs Node::set_wall_clock
and Node::peer_time_estimate, so no &mut Store escapes the facade.
Exchange::accept enqueued a pending ack whenever
!(duplicate && addressed && released) held, even when the backend
store.insert returned Err (store full / backend error). On Err the
released flag computed to false, so an ack was still enqueued: a
receiver that did not keep the message acked it. For an addressed
offer that releases the sender's custody for a message nobody holds;
for a broadcast it fires a false confidence event at the sender.
Gate the ack enqueue on res.is_ok(): enqueue only when the store
actually retained the observation. The existing released-holder
exception still applies among kept copies. No bounce is recorded on
Err (already the case).
Adds a unit test exercising a capped store (StoreError::Full) for
both a broadcast and an addressed offer, and a tines-sim property
that drives custody with CappedStore-backed nodes and asserts the
"at least one holder" invariant still holds when stores fill.
Revert on_ack release gating to release-on-any-ack (custody chain): release
custody on the first exact Ack from ANY peer the node dispatched the
observation to (relay or recipient), not just the recipient.
drain_ack_buffer send_n==0 bug: re-keep only the target addr's own pulled
entries instead of removing buffer.entries[0] (which dropped an unrelated
peer's oldest pending ack).
Rename the tines-sim addressed proptest to
addressed_always_held_by_carrier_or_recipient and reconcile its assertion to
the custody-chain holder invariant. Reword the misleading "Ids shrink
monotonically" comment. Add unit tests: ack_releases_custody_from_a_relay and
drain_ack_buffer_overflow_drops_only_target_peer.
Gate green: tines-core 106 lib tests, tines-sim suite, sh scripts/ci.sh.
Buffer pending acks (acks for addressed, bcast_acks for broadcast,
ACK_BUFFER_CAP=256, overflow drops oldest), skip a released-duplicate
offer in accept(), and emit Ack frames in next_frame after a due summary
and before addressed/broadcast traffic, draining up to ACK_IDS_CAP ids
per destination and splitting across frames to fit mtu.
Release custody on an Ack only when the Ack confirms delivery to the
intended recipient: an Ack from a peer that merely holds an
epidemic-forwarded copy no longer releases custody, so the local copy
stays available to forward toward the recipient once the only contact to
it closes.
Symlink the root Apache-2.0 LICENSE into tines-core and tines-sim so
`cargo package` includes the license text.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Share version, edition, license, authors, repository and readme through
[workspace.package]; add per-crate documentation, keywords and
categories. Fixes `cargo publish` warning about missing documentation,
homepage or repository.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Bump tines-core and tines-sim to 0.2.0 (Peer::in_flight changed type).
Add CHANGELOG.md. README gains a "Writing a store" section and a note on
calling Exchange::on_send_error when driving Exchange without Node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add Store::for_each_addressed_id (default: every id; MemStore filters)
so custody selection no longer clones every broadcast observation on
each poll.
- Factor fragment sizing into frag::plan and MAX_FRAGMENTS, used by both
frag::fragment and the exchange, and by the reassembler's bounds.
- Document `to` in the Observation::id hash input, matching the
addressed-delivery plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Cut every fragment of a stream from one encoding (age frozen at the
first fragment) and bounds-check the slice: re-encoding mid-stream
corrupted fragments and could panic when the tick went backwards.
- Fold the bloom summary to fit the MTU instead of sending a ~540-byte
frame that small-MTU links reject; drop the sim's summary MTU
exemption that hid this.
- Finish a peer's in-flight stream before starting another, and count
an addressed message as delivered only after its last fragment.
- Alternate custody and broadcast per peer, rotate through addressed
candidates, and evict the oldest bounce record when full, so neither
path starves.
- Add Exchange::on_send_error; Node::poll uses it so Busy/Unreachable
sends are retried rather than silently dropped.
- Clear bounce records per contact, record duplicates against their
sender, and skip addressed messages that cannot fit the MTU.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Run clippy, docs and tests for tines-core with --no-default-features,
gating the MemStore-based tests on `alloc` so they compile there. Fail on
rustdoc warnings, pass --locked everywhere, and pin the toolchain to
1.99.0. In the workflow, use read-only permissions, cancel superseded
runs, add a 20-minute timeout, and pin actions to commit SHAs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>