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>
This commit is contained in:
@@ -0,0 +1,157 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user