30 KiB
tines-core 1.0 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Ship tines-core 1.0.0: a versioned wire format, a frozen minimal public API, a settled Store trait, a reserved signature slot, and evidence (fuzzing, pinned vectors, MSRV, semver-checks) that the untrusted-input and compatibility promises hold.
Architecture: 1.0 adds nothing to the exchange's behaviour; it removes, hides, pins, documents and reserves. Two wire-format changes (a version byte, then the Observation reshape) plus a Store trait extension and four core-hardening fixes, each landed as an independently green, committed task on top of master (aedb802).
Tech Stack: Rust 2021, #![no_std] + optional alloc, postcard, heapless, blake2, serde, proptest, cargo-fuzz (nightly), cargo-msrv, cargo-semver-checks.
Spec: docs/superpowers/specs/2026-10-18-tines-1-0-design.md (draft v6, approved by Kyle 2026-10-20) — the plan argues from the spec, so the spec travels with it; executors read both. Handoff notes: HANDOFF.md.
Global Constraints
Every task's requirements implicitly include this section.
tines-corestays#![no_std];allocan optional default-on feature;--no-default-featuresbuilds forthumbv7em-none-eabihf. No async/threads/I/O/panics on untrusted input; wire frames are untrusted, malformed bytes becomeErr/a counted drop, never a panic.Addris never mapped toNodeIdby core.- Identity hash never changes:
id()readsdest.map(|d| d.to)and is byte-for-byte the 0.x hash; the pinned vector (below) carries over unchanged.sigis excluded from the id. - Exact values:
MAX_TOPIC=64,MAX_PAYLOAD=1024,MAX_PEERS=8,SUMMARY_BYTES=512,BLOOM_K=3,SUMMARY_INTERVAL_MS=2000,REASSEMBLY_TIMEOUT_MS=5000,SEND_QUEUE_CAP=256,MAX_FRAME=1500,ACK_IDS_CAP=90,EVENT_QUEUE_CAP=64,BCAST_SENT_CAP=256,ACK_BUFFER_CAP=256,MAX_SIG=64,FRAG_OVERHEAD(currently 64, becomes 65 in Task 1, private in Task 5),SUMMARY_OVERHEAD(private, currently 24, becomes 25 in Task 1). - Decisions already made (do not reopen): D1 reserve signature slot; D2
Destreplacesto+lifetime_ms; D3 drop-and-report; D4 onlytines-coregoes 1.0; D5 no store migration (old logs refused, wiped); Q1MAX_SIG=64; Q2 coverage = whole observation +version+algo; Q3 malformed reported like version mismatch; R1 drop reports carry transportAddr; R2 drops counted by(Addr, reason), not queued; R3MAX_FRAME=1500stays; R4observed_at= observer's wall-clock ms since Unix epoch; FStore::flush+Node::flush(now)replacesstore_mut; G age across downtime via a wall-clock marker. tines-ros2is not in the default gate (scripts/ci.shruns--workspacebut theros2feature is off; verify any change touchingtines-ros2withdocker build -f Dockerfile.ros2 -t tines-ros2-check .— cached image exists).- Gate (
sh scripts/ci.sh, must be green at every commit):cargo fmt --all -- --check→cargo clippy --workspace --all-targets -- -D warnings→cargo doc --workspace --no-deps→cargo test --workspace→sh scripts/check-no-std.sh. - House rule: primary branch is
master; land by rebase + fast-forward. Commit messages use the conventional prefix shown in each task; license Apache-2.0, copyright "K. Isom". - Execution: local
ornith-1.5-35b-a3bsubagents, reviews run sequentially (single-GPU llama-server). When a task touchestines-ros2, its final step is the Docker build, not a plaincargo check.
Review Focus
The five failure modes the spec implies but no single task's tests exercise, most likely to bite first:
- A frame with the wrong version byte (un-updated or foreign peer) must be dropped and reported as
VersionMismatch { version }, never silently swallowed and never folded intoMalformed. → pinned in Task 2. - A frame whose
fromchanges within one contact must be dropped asIdentityChangedand the peer's addressed mail must not be handed over to the new identity. → pinned in Task 9. - A signature with an unknown algorithm id, or the wrong length for a known one — the first must be carried and forwarded untouched; the second (and
algo = 0) isMalformed. → pinned in Task 4. - A store opened with unknown downtime must expire held addressed messages (never deliver a directive that may have outlived
lifetime_ms) and floor held broadcasts atunknown_downtime_age_ms, not deliver either as-is. → pinned in Task 10. - A valid frame with trailing bytes must be rejected as
Malformed(canonical decode), and a frame whose decode finds thefromfield missing must not panic. → pinned in Task 9. - Every frame, including a maximum-size signed observation, still fits
mtu()after the +1 version byte and the +67-byte signature. → pinned in Task 4.
File Map (decomposition locked in)
tines-core/src/wire.rs— envelope encode/decode,frame_sender;Frame/WireErrorbecome private in Task 5.tines-core/src/observation.rs—Observation,Dest,Signature,SigAlgo,NodeId,ObservationId,Tick,Topic,Payload.tines-core/src/consts.rs—WIRE_VERSION+ the wire/local split.tines-core/src/exchange.rs— drop counters,NodeEvent, peer pinning, hardening. Stays private (pub(crate)from Task 5).tines-core/src/node.rs—Nodefacade:flush(now),drain_events -> NodeEvent.tines-core/src/store.rs—Storetrait:is_empty,flush;store_conformancemodule.tines-core/src/lib.rs— the 1.0 public surface +#![deny(missing_docs)].tines-core/tests/vectors.rs— pinned identity + golden wire vectors.tines-store/src/log.rs— format v2 + wall-clock marker + downtime fold.tines-sim/src/capped.rs— theCappedStorewrapper (everyStorechange goes through it).tines/src/ipc.rs,tines-ros2/src/node.rs— host consumers ofNodeEvent/flush/observed_at.tines-bt/src/transport.rs—frame_senderimport site.fuzz/— cargo-fuzz target (outside the workspace).
Task 1: Wire envelope — WIRE_VERSION byte
Files:
- Modify:
tines-core/src/consts.rs(addWIRE_VERSION) - Modify:
tines-core/src/wire.rs(encode/decode prepend+check the byte;frame_senderbecomes version-aware; update the golden Summary vector) - Modify:
tines-core/src/exchange.rs(SUMMARY_OVERHEAD24 → 25) - Modify:
tines-core/src/frag.rs(FRAG_OVERHEAD64 → 65) - Modify:
tines-bt/tests/transport.rs,tines-bt/tests/adv.rs(theframe(from, payload)helpers prependWIRE_VERSION)
Interfaces:
-
Produces:
pub const WIRE_VERSION: u8 = 4;(decided: 4 is the first value in 4–0x7F, a single-byte postcard tag, ≥ the 0.x tags 0–3);WireErrorgainsVersionMismatch { version: u8 };frame_sender(bytes: &[u8]) -> Option<NodeId>now skips two leading bytes (version + variant tag) and returnsNonewhenbytes[0] != WIRE_VERSION. -
Step 1: Write the failing tests
In tines-core/src/wire.rs (mod tests): encode_prepends_version_byte (encode a Summary, assert buf[0] == WIRE_VERSION and Frame::decode(&buf[1..]) still decodes); decode_wrong_version_is_mismatch (a frame with buf[0] = WIRE_VERSION + 1 returns Err(WireError::VersionMismatch { version })); decode_empty_is_error (Frame::decode(&[]) is an Err, not a panic); frame_sender_skips_version_and_tag (bytes [WIRE_VERSION, 0, from..] yields Some(from)); frame_sender_wrong_version_is_none ([WIRE_VERSION+1, 0, from..] → None).
-
Step 2: Run, expect failure —
cargo test -p tines-core wirefails on the new names/behaviour. -
Step 3: Implement — add
WIRE_VERSION; inFrame::encodewriteout[0] = WIRE_VERSIONthen postcard intoout[1..](return1 + len,TooLargeifout.len() < 1 + len); inFrame::decodecheckbytes[0] == WIRE_VERSION(empty or mismatch →Err(VersionMismatch{version: bytes.first().copied().unwrap_or(0)}), then postcard the remainder, empty remainder required is not yet enforced — that is Task 9); rewriteframe_sendertoENVELOPE_OVERHEAD = 2. -
Step 4: Run, expect pass —
cargo test -p tines-coregreen; then update the tines-bt test helpers and re-runcargo test -p tines-bt. -
Step 5: Commit —
git commit -m "feat(core): WIRE_VERSION byte on every frame".
Task 2: Drop reporting — NodeEvent and the drop counter table (§1, §3 events)
Files:
- Modify:
tines-core/src/exchange.rs(renameConfidenceEvent→NodeEvent<A>; addDropReason, the counter table, the overflow counter; report drops inon_frame;drain_eventsreturnsNodeEvent) - Modify:
tines-core/src/node.rs(drain_eventssignature) - Modify:
tines-core/src/lib.rs(exportNodeEvent,DropReason; dropConfidenceEvent) - Modify:
tines-core/src/consts.rs(DROP_TABLE_CAP) - Modify:
tines/src/ipc.rs(mapNodeEvent::DeliveredintoEventOut;NodeEvent::Droppedis ignored by the IPC for now or surfaced as a stderr log) - Modify:
tines-ros2/src/node.rs+lib.rs(matchNodeEvent; fix the doc-comment links toConfidenceEvent)
Interfaces:
-
Consumes:
WireError::VersionMismatch(Task 1). -
Produces:
#[non_exhaustive] pub enum NodeEvent<A> { Delivered { peer: NodeId, id: ObservationId }, Dropped { addr: Option<A>, reason: DropReason, count: u32 } };#[non_exhaustive] pub enum DropReason { VersionMismatch { version: u8 }, Malformed }(IdentityChangedarrives in Task 9);pub const DROP_TABLE_CAP: usize = 32;(local const);Node::drain_events(&mut self) -> heapless::Vec<NodeEvent<T::Addr>, EVENT_QUEUE_CAP>. -
Step 1: Write the failing tests — in
exchange.rs(mod tests):version_mismatch_is_counted(drive one frame with a bad version byte fromaddr, drain, assert exactly oneDropped { addr: Some(addr), reason: VersionMismatch{..}, count: 1 });malformed_is_counted(garbage bytes →Dropped { reason: Malformed });same_drop_increments_count(two bad frames → oneDroppedwithcount: 2, drained once);drops_do_not_evict_delivered(fill the event queue withDelivered, add aDropped, assertDeliveredstill all present and theDroppedis the counter);overflow_counter_has_no_addr(drive >DROP_TABLE_CAPdistinct(addr, reason)pairs, assert the last is reported withaddr: None). -
Step 2: Run, expect failure —
cargo test -p tines-core exchangefails onNodeEvent/DropReasonnames. -
Step 3: Implement — rename to
NodeEvent<A>(two variants above); add the counter table (fixedheapless::Vec<(A, DropReason, u32), DROP_TABLE_CAP>or equivalent) + oneOption<(DropReason, u32)>overflow slot; inon_frame, replace theErr(_) => return Nonearm with a version-mismatch vs malformed branch that bumps the table;drain_eventsreturns queuedDeliveredfirst, then oneDroppedper nonzero counter (resetting it). -
Step 4: Run, expect pass —
cargo test -p tines-core, thencargo test -p tines, thendocker build -f Dockerfile.ros2 -t tines-ros2-check .(tines-ros2 consumers). -
Step 5: Commit —
git commit -m "feat(core): NodeEvent with counted drop reporting".
Task 3: Dest — the addressing reshape (§2)
Files:
- Modify:
tines-core/src/observation.rs(Dest;Observation.destreplacesto/lifetime_ms;id();aged(); constructorsbroadcast/addressed/with_observed_at; update the module's own tests) - Modify:
tines-core/src/node.rs(observe/observe_tobuild viadest;beliefreadsdest),store.rs(docs),mem_store.rs,exchange.rs(everyobs.to/obs.lifetime_ms→obs.dest;reject),wire.rs(Obsvariant + tests) - Modify:
tines-sim/src/capped.rs(only if it reads addressing — it doesn't, but re-verify it compiles),tines-sim/src/sim.rs/world.rs(anyto:/lifetime_ms:construction),tines-store/src/log.rs(Record encoding),tines/src/ipc.rs,tines-ros2/src/node.rs
Interfaces:
-
Consumes: nothing new.
-
Produces:
#[non_exhaustive] pub struct Dest { pub to: NodeId, pub lifetime_ms: u64 };Observation { pub observer, pub seq, pub age_ms, pub observed_at, pub topic, pub payload, pub dest: Option<Dest> }(sigis added by Task 4 — this task'sObservationhas nosigfield yet);Observation::broadcast(observer, seq, topic, payload) -> Observation,Observation::addressed(observer, seq, topic, payload, to, lifetime_ms) -> Observation,Observation::with_observed_at(mut self, at: u64) -> Observation. -
Step 1: Write the failing tests — in
observation.rs:dest_invariant_by_type(an addressed observation always hasdest: Some);id_reads_dest_to(obs(7,0,None,Some([2;16]),Some(100)).id() == obs(7,0,None,Some([2;16]),Some(200)).id()— lifetime still not part of identity — and the pinnedid_known_vectorvalue is unchanged);constructors_set_age_zero(broadcast/addressedproduceage_ms == 0and the rightdest);with_observed_at_sets_field. -
Step 2: Run, expect failure —
cargo test -p tines-core observationfails ondest. -
Step 3: Implement — mechanical:
dest: Option<Dest>replacesto: Option<NodeId>+lifetime_ms: Option<u64>;id()foldsself.dest.map(|d| d.to)(marker bytes unchanged);rejectbecomesobs.dest.is_some_and(|d| obs.age_ms > d.lifetime_ms)(the malformed-pair branch and its tests are deleted); update every downstream read/construct.cargo check --workspaceand fix all fallout. -
Step 4: Run, expect pass —
cargo test --workspacegreen;cargo build --workspacegreen. -
Step 5: Commit —
git commit -m "feat(core): Dest type replaces to + lifetime_ms".
Task 4: Signature + SigAlgo — the reserved slot (§2a)
Files:
- Modify:
tines-core/src/observation.rs(Signature,SigAlgo,Observation.sig,MAX_SIG) - Modify:
tines-core/src/consts.rs(MAX_SIG = 64in the wire group) - Modify:
tines-core/src/wire.rs(Obs/Fragencode/decode ofsig; the golden Obs vector) - Modify:
tines-core/tests/vectors.rs(golden Obs vector, both the Ed25519-shaped and unknown-algo cases)
Interfaces:
-
Consumes:
Observation(Task 3). -
Produces:
pub const MAX_SIG: usize = 64;;#[non_exhaustive] pub struct Signature { pub version: u8, pub algo: SigAlgo, pub bytes: heapless::Vec<u8, MAX_SIG> };#[non_exhaustive] #[repr(u8)] pub enum SigAlgo { Ed25519 = 1, Unknown(u8) }(decided: catch-all isUnknown(u8));algoserializes as a rawu8on the wire, not a postcard enum tag. -
Step 1: Write the failing tests — in
wire.rs/vectors.rs:obs_roundtrip_with_ed25519_signature(anObservationcarryingSignature { version: 1, algo: Ed25519, bytes: 64 bytes }decodes equal);obs_roundtrip_with_unknown_algo(algo: Unknown(7), short bytes, decodes and is forwarded, not rejected);algo_zero_is_malformed(algo: Unknown(0)→Malformed);ed25519_wrong_length_is_malformed(algo: Ed25519, 63 bytes →Malformed);sig_excluded_from_id(two observations identical exceptsigshare oneid());max_signed_obs_fits_mtu(Review Focus #6: aMAX_PAYLOADobservation with a 64-byte signature encodes withinMAX_FRAME). -
Step 2: Run, expect failure — tests fail on missing
Signature/SigAlgo. -
Step 3: Implement — the types above;
sig: Option<Signature>onObservation(no public setter in 1.0; core's decode sets it); wire:algoas rawu8(customSerialize/Deserializeor a#[serde(with = "…")]module that writes therepr(u8)discriminant;Unknown(n)writesn);version = 0and a wrong length forEd25519areMalformed;id()ignoressig. -
Step 4: Run, expect pass —
cargo test -p tines-coregreen; pin the golden Obs vector. -
Step 5: Commit —
git commit -m "feat(core): reserved Signature slot on Observation".
Task 5: Public API narrowing (§3)
Files:
- Modify:
tines-core/src/lib.rs(the 1.0 export list;mod wire;private,pub use wire::frame_sender;) - Modify:
tines-core/src/wire.rs,exchange.rs,frag.rs,bloom.rs(mark the non-API itemspub(crate)) - Modify:
tines-core/src/observation.rs,store.rs,transport.rs,node.rs(add#[non_exhaustive]where §3 requires) - Modify:
tines-sim/src/world.rs(fix the[Exchange]doc link),tines-bt/src/transport.rs(importtines_core::frame_sender) - Modify:
tines-bt/tests/transport.rs,adv.rs(import path)
Interfaces:
-
Consumes: all prior types.
-
Produces: the exact §3 public list.
Frame,WireError,Exchange,Peer,InFlight,Bloom(and itssaltfield),Reassembler,fragment,FRAG_OVERHEADarepub(crate).#[non_exhaustive]onObserveError,StoreError,SendError,Event,NodeEvent,DropReason,SigAlgo,Observation,Dest,Signature,TrimPolicy.pub use wire::frame_sender;at crate root (decided over keepingwirepublic). -
Step 1: Write the failing tests — a compile-time doc/export check: in
tines-core/tests/public_api.rs(or theconformancefeature), apub use-only probe thatuses each item on the §3 public list and asserts nothing; the failing signal iscargo check -p tines-core(private items are no longer importable — existing external callers break, which the plan fixes in this task). -
Step 2: Run, expect failure —
cargo check --workspacefails ontines-sim/tines-btimporting now-private items. -
Step 3: Implement — apply
pub(crate)/puband#[non_exhaustive]exactly per §3; fix the two external doc-link/import sites; runcargo doc --workspace --no-depsto confirm no broken private-link warnings. -
Step 4: Run, expect pass —
cargo check --workspace+cargo test --workspacegreen. -
Step 5: Commit —
git commit -m "refactor(core): narrow the 1.0 public API".
Task 6: The Store trait, settled (§4)
Files:
- Modify:
tines-core/src/store.rs(is_emptydefault,flushdefault, release docs, newstore_conformancemodule behind aconformancefeature) - Modify:
tines-core/src/node.rs(flush(now)replacesstore_mut) - Modify:
tines-core/Cargo.toml(conformancefeature) - Modify:
tines-store/src/log.rs(implementflush),tines-sim/src/capped.rs(delegate/implementis_empty/flush),tines/src/ipc.rs(callnode.flush(now)at line 214)
Interfaces:
-
Consumes: nothing new.
-
Produces:
Store::is_empty(&self) -> bool(defaultself.len() == 0; drop thelen_without_is_emptyallow);Store::flush(&mut self, now: Tick) -> Result<(), StoreError>(defaultOk(()));Node::flush(&mut self, now: Tick) -> Result<(), StoreError>;Node::store_mutremoved. Aconformancefeature gatingtines_core::store_conformance(ano_std-clean suite of shared tests exercising dedupe, age lineage, saturating ticks, release-surviving-get,siground-trip, trim order, no tombstones, idempotentflush). -
Step 1: Write the failing tests —
tines-core/tests/store_conformance.rs(featureconformance): each property above as a#[test]that runs against a provided backend; wireMemStore,tines_store::LogStoreandtines_sim::CappedStoreto it in their own crates. -
Step 2: Run, expect failure —
cargo test -p tines-core --features conformancefails onflush/is_empty/store_conformance. -
Step 3: Implement — trait defaults;
Node::flush(now)delegating toStore::flush;LogStore::flush(&mut self, now)writing aRecord::Clockwith the caller'snow(fixing F18's stale-marker); deletestore_mut; updateipc.rs:214tonode.flush(clock.now())(and thePollExchangercall sites). -
Step 4: Run, expect pass —
cargo test --workspace+docker build -f Dockerfile.ros2 -t tines-ros2-check .. -
Step 5: Commit —
git commit -m "feat(core): settle the Store trait (flush, is_empty, conformance suite)".
Task 7: Constants — wire vs local split (§5)
Files:
- Modify:
tines-core/src/consts.rs(re-group + document each const as wire or local) - Modify:
AGENTS.md(the constant list shows the split)
Interfaces:
-
Produces:
consts.rssplit into two documented groups — wire (frozen for v1:MAX_TOPIC,MAX_PAYLOAD,MAX_FRAME,SUMMARY_BYTES,ACK_IDS_CAP,MAX_SIG,BLOOM_K, and the privateMAX_FRAGMENTS) vs local (MAX_PEERS,SEND_QUEUE_CAP,ACK_BUFFER_CAP,BCAST_SENT_CAP,EVENT_QUEUE_CAP,SUMMARY_INTERVAL_MS,REASSEMBLY_TIMEOUT_MS,DROP_TABLE_CAP). -
Step 1: No new tests — this task only re-documents; the test is
cargo doc --workspace --no-depsstaying clean and the AGENTS.md list matchingconsts.rs. -
Step 2: Implement — move + document per §5's rule ("wire if changing it changes how a v1 peer decodes or interprets a frame").
-
Step 3: Verify —
cargo test --workspacegreen (no behaviour change). -
Step 4: Commit —
git commit -m "docs(core): split wire vs local constants".
Task 8: Bounce-table overflow, pinned (§6)
Files:
- Modify:
tines-sim/src/sim.rs,tines-sim/tests/props.rs(a property driving the table pastSEND_QUEUE_CAP)
Interfaces:
-
Consumes: the exchange's bounce table (unchanged).
-
Produces: a proptest that floods >
SEND_QUEUE_CAPdistinct observations acrossMAX_PEERSpeers and asserts the "always ≥ 1 holder" invariant and that the custody cursor still gives every candidate a turn. -
Step 1: Write the failing test —
tines-sim/tests/props.rs::bounce_overflow_keeps_a_holder(drive custody past capacity; assert ≥1 holder for each id and no candidate starved). -
Step 2: Run, expect pass or a real finding — this pins behaviour; if it fails, the finding is the deliverable (do not "fix" by weakening the invariant).
-
Step 3: Implement any needed fix — only if the property exposes a genuine ≥1-holder violation; §6 says the current drop-oldest design is already safe, so expect pass-as-is.
-
Step 4: Commit —
git commit -m "test(sim): pin bounce-table overflow behaviour".
Task 9: Core hardening — F6, F8, F9, F13 (§10)
Files:
- Modify:
tines-core/src/exchange.rs(peeridpinning; idle eviction; summary replay limit; drop-oldest events; pending-ack purge; canonical decode) - Modify:
tines-core/src/consts.rs(PEER_IDLE_MS = 30000, local)
Interfaces:
-
Consumes:
NodeEvent,DropReason(Task 2). -
Produces:
DropReason::IdentityChanged;pub const PEER_IDLE_MS: u64 = 30_000;. -
Step 1: Write the failing tests —
id_changes_are_dropped_as_identity_changed(frame 1 setspeer.id = A, frame 2 claimsfrom = B→ dropped, countedIdentityChanged, and B receives no addressed mail);idle_peer_is_evicted(a peer with no inbound frame for >PEER_IDLE_MSis removed as ifLost);replayed_summary_is_ignored(two summaries from one peer withinSUMMARY_INTERVAL_MS→ the second is not acted on and produces no re-send);events_drop_oldest(overflow the event queue → the oldestDeliveredis gone, newest retained);lost_peer_purges_pending_acks(pending acks to a removed peer are cleared);trailing_bytes_is_malformed(valid frame + one trailing byte →Malformed, Review Focus #5). -
Step 2: Run, expect failure — each test fails against current behaviour.
-
Step 3: Implement — F6: first decoded frame pins
peer.id, laterfrommismatch →IdentityChangeddrop (reset onContact/Lost); F8: track last-inbound tick, evict atPEER_IDLE_MS; F9: at most one summary per peer perSUMMARY_INTERVAL_MS, and skip already-sent ids on rebuild; F13:events.pushdrops the oldest (remove(0)), purge acks onLost/evict,take_from_bytes+ empty-remainder check. -
Step 4: Run, expect pass —
cargo test -p tines-core+ fullsh scripts/ci.sh. -
Step 5: Commit —
git commit -m "fix(core): peer pinning, idle eviction, replay and queue hardening".
Task 10: tines-store format v2 + age across downtime (§9, decision G)
Files:
- Modify:
tines-store/src/log.rs(bumpformat_version, wall-clock marker,open(path, now, wall_now), downtime fold, addressed expiry, broadcast floor, refuse old logs) - Modify:
tines-store/tests/persistence.rs(downtime tests) - Modify:
tines/src/main.rs+tines/src/ipc.rs,tines-ros2/src/node.rs(hosts passobserved_at+ trusted wall time)
Interfaces:
-
Consumes:
Observationwithobserved_at(Task 3),Store::flush(now)(Task 6). -
Produces:
LogStore::open(path: &Path, now: Tick, wall_now: Option<u64>) -> Result<Self, StoreError>; the log headerformat_versionbumps for the newObservationencoding;Record::Clockcarries an optional wall-clock ms; aLogStoreoption forunknown_downtime_age_ms. -
Step 1: Write the failing tests —
downtime_known_advances_ages(marker at wall 1000, reopen at 2000 → every held age +1000, saturating);downtime_unknown_expires_addressed(missing wall → held addressed obs never delivered, count reported once);downtime_unknown_floors_broadcasts(held broadcast age raised tounknown_downtime_age_ms);old_format_is_refused(a v1 log →Errnaming the fix);clock_went_backwards_is_unknown(marker ahead ofnow→ treated as unknown). -
Step 2: Run, expect failure —
cargo test -p tines-storefails on the new signature/behaviour. -
Step 3: Implement — per §9; hosts set
observed_at(None→Some(host wall ms)) and passwall_now; when a host has no trusted clock it passesNoneand configures the floor. -
Step 4: Run, expect pass —
cargo test -p tines-store+docker build -f Dockerfile.ros2 -t tines-ros2-check .. -
Step 5: Commit —
git commit -m "feat(store): log v2 with wall-clock marker and downtime fold".
Task 11: Evidence — pinned vectors + MSRV (§7)
Files:
- Create:
tines-core/tests/vectors.rs(consolidateid_known_vector+ the golden wire vectors) - Modify:
Cargo.toml([workspace.package] rust-version),scripts/ci.sh(MSRV job)
Interfaces:
-
Consumes: final wire encodings (Tasks 1–4).
-
Produces:
tines-core/tests/vectors.rswith the identity vector (unchanged value) + one golden encoding perFramevariant including the version byte + the Ed25519/unknown-algo Obs vectors + a 0.x-shaped frame rejected as version mismatch;rust-versionset to the lowest version that buildstines-core(found viacargo-msrv, not guessed); a CI job building/testingtines-coreon that MSRV. -
Step 1: Write the failing tests — move the existing vectors in; add the missing golden encodings.
-
Step 2: Run, expect pass —
cargo test -p tines-core vectorsgreen. -
Step 3: Implement — find MSRV with
cargo msrv --min 1.70 --max 1.85(or current range); setrust-version; add the CI job. -
Step 4: Commit —
git commit -m "test(core): pinned vectors + MSRV gate".
Task 12: Evidence — fuzz harness + proptest twin (§7)
Files:
- Create:
fuzz/(cargo-fuzz, outside the workspace, nightly),fuzz/Cargo.toml,fuzz/fuzz_targets/node.rs - Modify:
tines-core/tests/(the proptest stable twin of the same harness) - Modify:
.github/workflows/(scheduled fuzz job)
Interfaces:
-
Consumes:
Node,MemStore, a scriptedTransport. -
Produces: a fuzz target whose input decodes into
Contact/Lost/Frame(bytes)/Tick(delta, may go backwards)/observesteps; an adversarial-peer mode building well-formed frames (chosen summary bits, Obs/Frag ids/observers/ages/lifetimes, acks for sent/unsent ids, conflicting fragments, changingfrom) from many addresses beyondMAX_PEERS; post-step checks (no panic, ≤mtu(), no addressed stored past lifetime, drop counts reconcile, signatures round-trip). A proptest twin with a bounded case count runs in the normal suite. -
Step 1: Write the harness — the twin first (it runs in CI); then the fuzz target wrapping it.
-
Step 2: Run —
cargo test -p tines-core(twin green); a shortcargo fuzz run node -- -max_total_time=60sanity run (full 24 CPU-hour budget is release-time, §"Done means"). -
Step 3: Commit —
git commit -m "test(core): fuzz harness and proptest twin".
Task 13: Docs, compatibility policy, and the 1.0.0 release (§8, "Done means")
Files:
- Modify:
README.md(a "Stability" section with §8),CHANGELOG.md(1.0.0 entry),AGENTS.md(spec list + trait extension rule + wire/local split),tines-core/src/lib.rs(#![deny(missing_docs)]+ document the remaining items),THREAT_MODEL.md(mark F6/F8/F9/F13 fixed with SHAs),scripts/ci.sh(cargo-semver-checksgate),Cargo.toml+ dependent crates (tines-core→ 1.0.0, hosts depend on"1")
Interfaces:
-
Consumes: everything above.
-
Produces: README §8; CHANGELOG 1.0.0 listing every 0.3 break;
#![deny(missing_docs)]with zero missing items (recount — the spec's "26" predates the fleet fixes);THREAT_MODEL.mdmarks F6/F8/F9/F13 fixed;cargo-semver-checksinscripts/ci.sh;tines-coreat 1.0.0 withtines-store/tines-bt/tines-ros2/tinesdepending on"1". -
Step 1: Write the failing test —
cargo doc --workspace --no-depsunder#. -
Step 2: Implement — the docs; the release bump; the semver-check gate.
-
Step 3: Verify —
sh scripts/ci.shgreen including the semver-checks job;THREAT_MODEL.mdmarks F6/F8/F9/F13 fixed with commit SHAs. -
Step 4: Commit —
git commit -m "release(core): tines-core 1.0.0".