Seven task files for the implementing model under docs/plans/M1/, with the test files, byte-exact fixtures, Makefile, deny.toml and gate-script self-test they copy into place. All of it was verified against a private reference implementation: the gate passes after every task in order. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5.9 KiB
M1 task 02: proto identifiers and values
Branch: m1 (run git switch m1; git status --short must be empty, otherwise stop)
Commit subject: Add validated identifiers, Hash32, Timestamp and DataClass to proto
Goal
Add the small validated value types that every later type is built from. They are defined by
crates/proto/tests/ids.rs, which you copy in and must not edit.
Context
From the spec: "Identifiers. Constructors and Deserialize validate; invalid input is an error,
never a panic." A SessionId becomes a directory name, so it must never contain / or ..
Every value has exactly one spelling in JSON, because log files are compared and hashed as bytes.
Files
- Copy:
crates/proto/tests/ids.rs - Create:
crates/proto/src/ids.rs,crates/proto/src/class.rs - Modify:
Cargo.toml,crates/proto/Cargo.toml,crates/proto/src/lib.rs,docs/dependencies.md,docs/implementer-log.md,Cargo.lock(generated)
Interfaces
Produces, all re-exported from the crate root (proto::SessionId and so on):
// crates/proto/src/ids.rs
pub enum ValueError { SessionId, Hash32, Timestamp } // Debug, Clone, Copy, PartialEq, Eq; Display; std::error::Error
pub struct SessionId(String); // 1 to 64 bytes, each one of a-z, 0-9 or '-'
impl SessionId {
pub fn new(s: &str) -> Result<Self, ValueError>; // Err(ValueError::SessionId)
pub fn as_str(&self) -> &str;
}
pub struct Epoch(pub u32); // JSON: a plain number
pub struct CallId(pub u64); // JSON: a plain number
pub struct Hash32([u8; 32]); // JSON: exactly 64 lowercase hex characters
impl Hash32 {
pub const ZERO: Hash32; // 32 zero bytes
pub fn from_bytes(bytes: [u8; 32]) -> Self;
pub fn as_bytes(&self) -> &[u8; 32];
pub fn to_hex(&self) -> String;
pub fn from_hex(s: &str) -> Result<Self, ValueError>; // Err(ValueError::Hash32); uppercase is an error
}
pub struct Timestamp(u64); // Unix milliseconds; JSON: "2026-09-17T08:05:00.000Z"
impl Timestamp {
pub fn from_unix_millis(ms: u64) -> Self;
pub fn unix_millis(&self) -> u64;
pub fn now() -> Self;
pub fn to_rfc3339(&self) -> String; // always three decimals and a final Z
pub fn parse(s: &str) -> Result<Self, ValueError>; // Err(ValueError::Timestamp)
}
// crates/proto/src/class.rs
pub enum DataClass { Public, Private, Secret } // JSON: "public", "private", "secret"
Derives: SessionId gets Debug, Clone, PartialEq, Eq, Hash. Epoch, CallId and Timestamp
get Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash. Hash32 gets
Debug, Clone, Copy, PartialEq, Eq, Hash. DataClass gets
Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, with the variants in the order shown so
that Public < Private < Secret. All of them get Serialize, Deserialize.
Rules the tests check:
Timestamp::parseaccepts only the spellingto_rfc3339produces. Parse the text, format the result again, and reject the input if the two strings differ.- Deserializing must validate. A JSON string that
new,from_hexorparsewould reject must fail to deserialize.
API notes (verified on docs.rs, 2026-09-17)
- serde container attributes:
#[serde(try_from = "String", into = "String")]makes a type serialize throughFrom<T> for Stringand deserialize throughTryFrom<String> for T; the error type must implementDisplay.intorequiresClone.#[serde(transparent)]makes a one-field struct serialize as that field.#[serde(rename_all = "lowercase")]on an enum. humantime::format_rfc3339_millis(t: std::time::SystemTime) -> humantime::Rfc3339Timestamp, which implementsDisplay.humantime::parse_rfc3339(s: &str) -> Result<std::time::SystemTime, humantime::TimestampError>.std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms)gives aSystemTime;t.duration_since(UNIX_EPOCH)gives aResult<Duration, _>;Duration::as_millis()isu128.
Steps
- 1. Copy the test and add the dependencies.
git switch m1
mkdir -p crates/proto/tests
cp docs/plans/M1/files/crates/proto/tests/ids.rs crates/proto/tests/
Add to [workspace.dependencies] in the root Cargo.toml:
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
humantime = "2.4.0"
Add to [dependencies] in crates/proto/Cargo.toml:
serde.workspace = true
serde_json.workspace = true
humantime.workspace = true
Add these rows to the table in docs/dependencies.md:
| `serde` | 1.0.229 | `proto` | Derives serialization for every shared type. MIT OR Apache-2.0. |
| `serde_json` | 1.0.151 | `proto` | JSON for frames and log files. MIT OR Apache-2.0. |
| `humantime` | 2.4.0 | `proto` | RFC 3339 timestamps. No dependencies of its own. MIT OR Apache-2.0. |
-
2. See the test fail.
cargo test -p proto --test ids. Expected: it does not compile, becauseproto::SessionIdand the other names do not exist. -
3. Write
class.rsandids.rs, and inlib.rsadd below the doc comment:
pub mod class;
pub mod ids;
pub use class::DataClass;
pub use ids::{CallId, Epoch, Hash32, SessionId, Timestamp, ValueError};
Write the hex encoding and decoding yourself; do not add a hex crate.
-
4. See the test pass.
cargo test -p proto --test ids. Expected:11 passed; 0 failed. -
5. Run the gate.
make gate. Expected last line:gate: ok. -
6. Log and commit.
git add Cargo.toml Cargo.lock crates/proto docs/dependencies.md docs/implementer-log.md
git commit
Done when
cargo test -p proto --test idsreports 11 passed.make gateprintsgate: ok.cmp crates/proto/tests/ids.rs docs/plans/M1/files/crates/proto/tests/ids.rsprints nothing.
Stop and report if
- A test in
ids.rsseems wrong to you. Do not edit it. humantimedoes not have the two functions named above.