Files
kyleandClaude Fable 5.1 3e26c2e3c0 Add M1 plan, given tests and fixtures, AGENTS.md and implementer log
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>
2026-09-17 01:22:43 -07:00

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::parse accepts only the spelling to_rfc3339 produces. 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_hex or parse would 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 through From<T> for String and deserialize through TryFrom<String> for T; the error type must implement Display. into requires Clone. #[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 implements Display.
  • humantime::parse_rfc3339(s: &str) -> Result<std::time::SystemTime, humantime::TimestampError>.
  • std::time::UNIX_EPOCH + std::time::Duration::from_millis(ms) gives a SystemTime; t.duration_since(UNIX_EPOCH) gives a Result<Duration, _>; Duration::as_millis() is u128.

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, because proto::SessionId and the other names do not exist.

  • 3. Write class.rs and ids.rs, and in lib.rs add 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 ids reports 11 passed.
  • make gate prints gate: ok.
  • cmp crates/proto/tests/ids.rs docs/plans/M1/files/crates/proto/tests/ids.rs prints nothing.

Stop and report if

  • A test in ids.rs seems wrong to you. Do not edit it.
  • humantime does not have the two functions named above.