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

158 lines
5.9 KiB
Markdown

# 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):
```rust
// 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.**
```sh
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`:
```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`:
```toml
serde.workspace = true
serde_json.workspace = true
humantime.workspace = true
```
Add these rows to the table in `docs/dependencies.md`:
```markdown
| `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:
```rust
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.**
```sh
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.