diff --git a/AGENTS.md b/AGENTS.md index 2bb0f9d..e26e1f0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,7 +42,8 @@ Unix sockets. You are implementing it one task at a time. ## The gate `make gate` must print `gate: ok` before a task is done. It runs offline: rustfmt, clippy with -warnings denied, all tests, cargo-deny, and the scripts in `scripts/`. After you add a dependency, +warnings denied, all tests, cargo-deny, and the scripts in `scripts/`. Run `cargo fmt --all` before +the gate; rustfmt decides the order of `mod` and `use` lines, not you. After you add a dependency, run `cargo build` once so that `Cargo.lock` is updated, then run the gate. Useful while working: `cargo test -p --test ` runs one test file, and diff --git a/docs/implementer-log.md b/docs/implementer-log.md index 98b8d96..4c5e4dc 100644 --- a/docs/implementer-log.md +++ b/docs/implementer-log.md @@ -14,3 +14,46 @@ reviewer adds findings under "Reviews" once per milestone. | M1/07-brokerd-decision | 2026-09-17 | done | 1 | pass | none | Added Decision (Debug only, private fields), decide (Err(NoGrant) until M3) and the run stub (ToolResponse::Failed) in crates/brokerd; Decision::new carries expect(dead_code). 2 unit + 3 doctests (2 compile_fail) pass; verified the compile_fail guards by temporarily making new pub. `make gate` prints `gate: ok`. | ## Reviews + +### M1, tasks 01 to 07 — reviewed 2026-09-17 by the design model (Claude) + +**Verdict: accepted, with two follow-up tasks (08, 09).** Nothing has to be redone. + +Checklist from `docs/plans/M1/README.md`: + +| Check | Result | +|---|---| +| Seven commits on `m1`, one per task, each with the `Implemented-By` trailer | pass | +| All 23 copied files (tests, fixtures, `Makefile`, `deny.toml`, self-test) byte-identical to the plan | pass | +| No change to the brief, specs, plans, `AGENTS.md`, `CLAUDE.md`; working tree clean; nothing pushed | pass | +| `make gate` | `gate: ok`, 45 tests | +| `make audit` | `advisories ok` | +| No `unwrap`, `expect`, `panic!`, `#[allow]` or `unsafe` in library code; no dependency the tasks did not name | pass | +| Field order, derives and signatures match the tasks | pass | + +Process: first gate run passed in 4 of 7 tasks. The three failures were two compile fixes (task 04) +and rustfmt reordering `lib.rs` re-exports (tasks 04 to 06). Commits run from 06:45 to 09:05. + +Findings. "Implementer" means the task said it and the code missed it. "Task" means the task or its +tests, written by the reviewer, were wrong or silent; the reference implementation had the same +defect in both such cases. + +| # | Severity | Owner | Finding | Fix | +|---|---|---|---|---| +| 1 | medium | implementer, and a gap in the given tests | `AuditRecord` and `ToolCall` lack `deny_unknown_fields`. An audit line with an extra `"forged":true` field decodes. The given tests only checked the enums. | Task 08; new `tests/strict.rs` checks every object at every depth | +| 2 | medium | task | `Timestamp::from_unix_millis` accepts any `u64`, but `to_rfc3339` and serialization panic above year 9999, because `humantime`'s `Display` returns an error and `to_string()` panics on that. | Task 08 | +| 3 | medium | task | `[dependencies.brokerd]` with `workspace = true` lets a role depend on another role while both dependency scripts pass. Dotted `brokerd.path = …` is also missed. The self-test had no such case. | Task 09 | +| 4 | low | implementer | All three scripts pass when `ROOT/crates` is missing, and hide tool errors with `2>/dev/null`. A gate check that cannot look must fail. | Task 09 | +| 5 | low | implementer | `check-lines.sh` does not print the line count. | Task 09 | +| 6 | nit | implementer | `frame.rs` uses bounded `as` casts where `try_from` would say the same without a second look. `read_bytes` has two match arms that do the same thing. `Constraints` has a needless `rename_all`. `ids.rs`, `class.rs`, `wire.rs` have no module doc comment. | Not worth a task; fix when next touched | +| 7 | low | task | The tasks told the implementer how to order `lib.rs` lines, and rustfmt disagreed, which cost three gate runs. | `AGENTS.md` now says to run `cargo fmt --all` before the gate | + +Open question for the owner: the task 01 row says the skeleton files were "already present +untracked from a prior attempt". The log has no row for that attempt, so its gate runs and the +reason it ended are not recorded. + +On the experiment (review once per milestone): it held up for M1. None of the defects was built on +by a later task, and all were found by reading the branch and probing it from outside. M1 is the +easy case, though: types pinned by byte-exact fixtures. M2 has behaviour that fixtures cannot pin +as tightly (a streaming HTTP client, the turn loop), so an early mistake there is more likely to +be built on. diff --git a/docs/plans/M1/08-proto-strictness.md b/docs/plans/M1/08-proto-strictness.md new file mode 100644 index 0000000..d5d9050 --- /dev/null +++ b/docs/plans/M1/08-proto-strictness.md @@ -0,0 +1,86 @@ +# M1 task 08: close two gaps in `proto` (review follow-up) + +**Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) +**Commit subject:** `Reject unknown fields in every proto struct and bound Timestamp` + +## Goal + +The M1 review found two defects in `proto`. Fix both. Each is defined by a test file that you copy +in and must not edit. + +1. `AuditRecord` and `ToolCall` accept unknown JSON fields. The rule is that unknown fields are + rejected everywhere. The reviewer's probe showed an audit line with an extra `"forged":true` + field decoding without error. +2. `Timestamp::from_unix_millis` accepts any `u64`, but `to_rfc3339` can only spell times up to + the year 9999. For a larger value, `humantime`'s formatter returns an error, `to_string()` + panics, and so does serializing the value. Library code must never panic. This one was a + mistake in the task you were given, not in your code. + +## Files + +- Copy (replacing the old one): `crates/proto/tests/ids.rs` +- Copy (new): `crates/proto/tests/strict.rs` +- Modify: `crates/proto/src/ids.rs`, `crates/proto/src/audit.rs`, `crates/proto/src/log.rs`, + `docs/implementer-log.md` +- Check, and modify only if the test says so: every other file in `crates/proto/src/` + +## Interfaces + +Changes to `Timestamp` in `crates/proto/src/ids.rs`. Everything else keeps its signature. + +```rust +impl Timestamp { + /// 9999-12-31T23:59:59.999Z, the last instant RFC 3339 can spell with a four-digit year. + pub const MAX: Timestamp; // 253_402_300_799_999 ms + + // Was: -> Self. Now fails with ValueError::Timestamp when ms > MAX. + pub fn from_unix_millis(ms: u64) -> Result; +} +``` + +Rules: + +- No `Timestamp` value above `MAX` can exist. `from_unix_millis` and `parse` reject such values, + and `now()` never produces one (clamp it to `MAX`; a clock that far off is not worth an error). +- With that guarantee `to_rfc3339` cannot fail. Keep it returning `String`. +- `strict.rs` takes every fixture, adds one unknown key to one JSON object at a time, at every + depth, and requires decoding to fail. If it reports a type other than `AuditRecord` or + `ToolCall`, fix that type too and say so in your log row. + +## Steps + +- [ ] **1. Copy the tests.** + +```sh +git switch m1 +cp docs/plans/M1/files/crates/proto/tests/ids.rs docs/plans/M1/files/crates/proto/tests/strict.rs crates/proto/tests/ +``` + +- [ ] **2. See them fail.** `cargo test -p proto --test strict`. Expected: 2 of 4 tests fail with + `accepted an unknown key`. `cargo test -p proto --test ids`. Expected: it does not compile, + because `Timestamp::MAX` does not exist and `from_unix_millis` does not return a `Result`. + +- [ ] **3. Fix the code.** + +- [ ] **4. See them pass.** `cargo test -p proto`. Expected: `ids` 12 passed, `strict` 4 passed, + and 45 passed in total across the six test files. + +- [ ] **5. Format, then run the gate.** `cargo fmt --all`, then `make gate`. Expected last line: + `gate: ok`. + +- [ ] **6. Log and commit.** + +```sh +git add crates/proto docs/implementer-log.md +git commit +``` + +## Done when + +- `cargo test -p proto` reports 45 passed. +- `make gate` prints `gate: ok`. +- `diff -r crates/proto/tests docs/plans/M1/files/crates/proto/tests` prints nothing. + +## Stop and report if + +- `strict.rs` fails for a type after you have added `deny_unknown_fields` to it. diff --git a/docs/plans/M1/09-gate-scripts-table-form.md b/docs/plans/M1/09-gate-scripts-table-form.md new file mode 100644 index 0000000..4278669 --- /dev/null +++ b/docs/plans/M1/09-gate-scripts-table-form.md @@ -0,0 +1,93 @@ +# M1 task 09: make the gate scripts fail closed (review follow-up) + +**Branch:** `m1` (run `git switch m1`; `git status --short` must be empty, otherwise stop) +**Commit subject:** `Make gate scripts handle table-form dependencies and fail closed` + +## Goal + +The M1 review found that the dependency checks can be bypassed by accident, and that all three +scripts pass when they cannot find what they are supposed to check. Fix the scripts so the +extended self-test passes. The self-test is copied in and must not be edited. + +The first problem was a gap in the task you were given: it did not mention that TOML lets a +dependency be written as its own table. The reviewer's probe added this to `crates/loopd/Cargo.toml` +(with `brokerd` also listed in `[workspace.dependencies]`), and both scripts passed although +`loopd` then depended on another role: + +```toml +[dependencies.brokerd] +workspace = true +``` + +## Files + +- Copy (replacing the old one): `scripts/test-gate-scripts.sh` +- Modify: `scripts/check-lines.sh`, `scripts/check-crate-deps.sh`, `scripts/check-dep-docs.sh`, + `docs/implementer-log.md` + +## Required behaviour + +Everything from task 01 still holds. In addition: + +1. **A dependency's name can appear in three places.** All three count, in any section whose name + ends in `dependencies` (`[dependencies]`, `[dev-dependencies]`, `[build-dependencies]`, + `[target.'cfg(unix)'.dependencies]`): + - a plain key: `brokerd = { path = "../brokerd" }` or `brokerd = "1"` + - a dotted key: `brokerd.workspace = true` or `brokerd.path = "../brokerd"` + - a table header whose last part is the name: `[dependencies.brokerd]` or + `[dev-dependencies.brokerd]`. The lines inside that table (`path = …`, `workspace = true`, + `features = […]`) are settings of that dependency, not dependencies themselves. +2. **`check-crate-deps.sh`** applies its rule to names found in all three places. +3. **`check-dep-docs.sh`**, second rule: a plain or dotted dependency must have `workspace = true` + on its line, as before. A table-form dependency must have a `workspace = true` line inside its + table. `[dependencies.serde]` followed by `workspace = true` and `features = ["derive"]` is + fine; `[dependencies.rand]` followed by `version = "0.9"` is an error. +4. **Fail closed.** Each script exits 1 with a message if `ROOT/crates` does not exist. + `check-dep-docs.sh` also exits 1 if `ROOT/Cargo.toml` or `ROOT/docs/dependencies.md` does not + exist. Do not hide errors from `find`, `awk` or `grep` with `2>/dev/null`: if a tool fails, the + script must not report success. +5. `check-lines.sh` prints the line count of each file that is too long, for example + `check-lines: crates/x/src/a.rs has 612 lines (limit 500)`. + +Still POSIX `sh` only: no bash features, no Python, no `jq`. + +## Steps + +- [ ] **1. Copy the self-test.** + +```sh +git switch m1 +cp docs/plans/M1/files/scripts/test-gate-scripts.sh scripts/ +``` + +- [ ] **2. See it fail.** `sh scripts/test-gate-scripts.sh`. Expected: 7 failures, among them + `role depends on another role, written as a table` and + `check-lines without a crates directory`. + +- [ ] **3. Fix the three scripts.** Read the new cases in `scripts/test-gate-scripts.sh` first; each + one has a comment or a name that says what it checks. + +- [ ] **4. See it pass.** `sh scripts/test-gate-scripts.sh`. Expected: `test-gate-scripts: ok`. + Then run each script on the real tree: `sh scripts/check-lines.sh`, + `sh scripts/check-crate-deps.sh`, `sh scripts/check-dep-docs.sh`. Expected: no output, exit 0. + +- [ ] **5. Run the gate.** `make gate`. Expected last line: `gate: ok`. + +- [ ] **6. Log and commit.** + +```sh +git add scripts docs/implementer-log.md +git commit +``` + +## Done when + +- `sh scripts/test-gate-scripts.sh` prints `test-gate-scripts: ok`. +- `make gate` prints `gate: ok`. +- `cmp scripts/test-gate-scripts.sh docs/plans/M1/files/scripts/test-gate-scripts.sh` prints + nothing. +- `grep -n '2>/dev/null' scripts/check-*.sh` prints nothing. + +## Stop and report if + +- A case in the self-test contradicts the rules above. diff --git a/docs/plans/M1/README.md b/docs/plans/M1/README.md index 20c2b43..6316ac3 100644 --- a/docs/plans/M1/README.md +++ b/docs/plans/M1/README.md @@ -1,7 +1,7 @@ # M1 implementation plan: workspace, `proto`, gate > **For the implementing model:** do not work from this file. The owner gives you one task file at -> a time (`01-…` to `07-…`). This file is the index for the owner and the reviewer. +> a time (`01-…` to `09-…`). This file is the index for the owner and the reviewer. **Goal:** A Cargo workspace whose gate passes, with the real shared types in `proto` and a `Decision` type in `brokerd` that code outside its policy module cannot construct. @@ -24,7 +24,8 @@ and verified against a private reference implementation; the implementer writes `docs/dependencies.md`. Crates are `publish = false`. - Unknown fields are rejected everywhere. Field order is the wire format. - `make gate` runs offline and must print `gate: ok` at the end of every task. -- Branch `m1`. One task, one fresh OpenCode session, one commit. Review happens once, after task 07. +- Branch `m1`. One task, one fresh OpenCode session, one commit. Review happened once, after task + 07; tasks 08 and 09 are its follow-ups. ## Tasks @@ -37,6 +38,8 @@ and verified against a private reference implementation; the implementer writes | 05 | `05-proto-grant.md` | `Grant`, `Mode`, `Constraints` | `tests/grant.rs`, `fixtures/grant/` | | 06 | `06-proto-records.md` | `DecisionRecord`, `AuditRecord`, `ToolCall`, `LogRecord` | `tests/records.rs`, `fixtures/records/` | | 07 | `07-brokerd-decision.md` | `brokerd::policy::Decision`, `decide`, `brokerd::runner::run` | doctests and unit tests in `policy.rs` | +| 08 | `08-proto-strictness.md` | Review follow-up: unknown fields rejected in every struct; `Timestamp` bounded at year 9999 | `tests/strict.rs`, updated `tests/ids.rs` | +| 09 | `09-gate-scripts-table-form.md` | Review follow-up: gate scripts see table-form and dotted dependencies, and fail closed | updated `scripts/test-gate-scripts.sh` | `files/` holds everything the tasks copy into place. Tests for a later task do not compile until that task's types exist, which is why they are copied task by task and not all at once. diff --git a/docs/plans/M1/files/crates/proto/tests/ids.rs b/docs/plans/M1/files/crates/proto/tests/ids.rs index 9291f66..ba7f6e6 100644 --- a/docs/plans/M1/files/crates/proto/tests/ids.rs +++ b/docs/plans/M1/files/crates/proto/tests/ids.rs @@ -100,7 +100,7 @@ fn hash32_rejects_wrong_length_uppercase_and_non_hex() { #[test] fn timestamp_has_one_spelling() { - let t = Timestamp::from_unix_millis(1_789_632_300_000); + let t = Timestamp::from_unix_millis(1_789_632_300_000).unwrap(); assert_eq!(t.unix_millis(), 1_789_632_300_000); assert_eq!(t.to_rfc3339(), "2026-09-17T08:05:00.000Z"); assert_eq!(Timestamp::parse("2026-09-17T08:05:00.000Z").unwrap(), t); @@ -109,11 +109,13 @@ fn timestamp_has_one_spelling() { r#""2026-09-17T08:05:00.000Z""# ); assert_eq!( - Timestamp::from_unix_millis(0).to_rfc3339(), + Timestamp::from_unix_millis(0).unwrap().to_rfc3339(), "1970-01-01T00:00:00.000Z" ); assert_eq!( - Timestamp::from_unix_millis(1_789_632_300_007).to_rfc3339(), + Timestamp::from_unix_millis(1_789_632_300_007) + .unwrap() + .to_rfc3339(), "2026-09-17T08:05:00.007Z" ); } @@ -141,6 +143,29 @@ fn timestamp_rejects_other_spellings() { assert!(serde_json::from_str::("1789632300000").is_err()); } +#[test] +fn timestamp_range_ends_with_year_9999() { + assert_eq!(Timestamp::MAX.unix_millis(), 253_402_300_799_999); + assert_eq!(Timestamp::MAX.to_rfc3339(), "9999-12-31T23:59:59.999Z"); + assert_eq!( + Timestamp::parse("9999-12-31T23:59:59.999Z").unwrap(), + Timestamp::MAX + ); + assert_eq!( + Timestamp::from_unix_millis(253_402_300_799_999).unwrap(), + Timestamp::MAX + ); + // One millisecond later has no RFC 3339 spelling, so it must not become a Timestamp at all. + assert_eq!( + Timestamp::from_unix_millis(253_402_300_800_000), + Err(ValueError::Timestamp) + ); + assert_eq!( + Timestamp::from_unix_millis(u64::MAX), + Err(ValueError::Timestamp) + ); +} + #[test] fn timestamp_now_is_after_2026() { assert!(Timestamp::now() > Timestamp::parse("2026-01-01T00:00:00.000Z").unwrap()); diff --git a/docs/plans/M1/files/crates/proto/tests/strict.rs b/docs/plans/M1/files/crates/proto/tests/strict.rs new file mode 100644 index 0000000..fcf0bb7 --- /dev/null +++ b/docs/plans/M1/files/crates/proto/tests/strict.rs @@ -0,0 +1,109 @@ +//! Every JSON object in every fixture must reject an unknown key. Do not edit. +//! +//! The other test files check unknown fields in a few hand-picked places. This one checks all of +//! them: it walks each fixture, adds one unknown key to one object at a time, at every depth, and +//! requires that the result no longer decodes. + +use proto::{AuditRecord, Envelope, Grant, LogRecord}; +use serde::de::DeserializeOwned; +use serde_json::Value; + +/// Every copy of `value` that has exactly one extra key in exactly one object. +fn with_one_unknown_key(value: &Value) -> Vec { + let mut out = Vec::new(); + match value { + Value::Object(map) => { + let mut extended = map.clone(); + extended.insert("zz_unknown".to_string(), Value::Bool(true)); + out.push(Value::Object(extended)); + for (key, child) in map { + for changed in with_one_unknown_key(child) { + let mut copy = map.clone(); + copy.insert(key.clone(), changed); + out.push(Value::Object(copy)); + } + } + } + Value::Array(items) => { + for (i, child) in items.iter().enumerate() { + for changed in with_one_unknown_key(child) { + let mut copy = items.clone(); + copy[i] = changed; + out.push(Value::Array(copy)); + } + } + } + _ => {} + } + out +} + +/// Returns how many variations were tried, so callers can check the walk reached nested objects. +fn check(what: &str, text: &str) -> usize { + let value: Value = serde_json::from_str(text).unwrap_or_else(|e| panic!("{what}: {e}")); + assert!( + serde_json::from_value::(value.clone()).is_ok(), + "{what}: fixture must decode" + ); + let variations = with_one_unknown_key(&value); + for changed in &variations { + assert!( + serde_json::from_value::(changed.clone()).is_err(), + "{what}: accepted an unknown key: {changed}" + ); + } + variations.len() +} + +fn fixture(path: &str) -> String { + let full = format!("{}/tests/fixtures/{path}", env!("CARGO_MANIFEST_DIR")); + std::fs::read_to_string(&full).unwrap_or_else(|e| panic!("{full}: {e}")) +} + +#[test] +fn envelopes_reject_unknown_keys_at_every_depth() { + for name in [ + "tool_request.json", + "tool_response_pending.json", + "tool_response_result.json", + "tool_response_failed.json", + "tool_response_denied.json", + "error.json", + ] { + // Envelope, msg and body: three objects. + assert_eq!( + check::(name, &fixture(&format!("wire/{name}"))), + 3, + "{name}" + ); + } +} + +#[test] +fn audit_records_reject_unknown_keys_at_every_depth() { + for (i, line) in fixture("records/audit.jsonl").lines().enumerate() { + // The record and its decision: two objects. + assert_eq!( + check::(&format!("audit.jsonl:{}", i + 1), line), + 2 + ); + } +} + +#[test] +fn log_records_reject_unknown_keys_at_every_depth() { + let mut tried = 0; + for (i, line) in fixture("records/session.jsonl").lines().enumerate() { + tried += check::(&format!("session.jsonl:{}", i + 1), line); + } + // Seven records, plus the one tool call inside the first assistant record. + assert_eq!(tried, 8); +} + +#[test] +fn grants_reject_unknown_keys_at_every_depth() { + let grant: Grant = toml::from_str(&fixture("grant/full.toml")).unwrap(); + let text = serde_json::to_string(&grant).unwrap(); + // The grant and its constraints: two objects. + assert_eq!(check::("full.toml as JSON", &text), 2); +} diff --git a/docs/plans/M1/files/scripts/test-gate-scripts.sh b/docs/plans/M1/files/scripts/test-gate-scripts.sh index f51d127..9e26746 100644 --- a/docs/plans/M1/files/scripts/test-gate-scripts.sh +++ b/docs/plans/M1/files/scripts/test-gate-scripts.sh @@ -63,6 +63,15 @@ tree "$tmp/c-dev"; printf '\n[dev-dependencies]\nbrokerd.workspace = true\n' >> expect fail "role dev-depends on another role" check-crate-deps.sh "$tmp/c-dev" tree "$tmp/c-proto"; manifest "$tmp/c-proto/crates/proto" proto 'loopd.workspace = true' expect fail "proto depends on a role" check-crate-deps.sh "$tmp/c-proto" +# A dependency can also be written as its own table: [dependencies.NAME] +tree "$tmp/c-sect"; printf '\n[dependencies.brokerd]\npath = "../brokerd"\n' >> "$tmp/c-sect/crates/loopd/Cargo.toml" +expect fail "role depends on another role, written as a table" check-crate-deps.sh "$tmp/c-sect" +tree "$tmp/c-sectw"; printf '\n[dev-dependencies.brokerd]\nworkspace = true\n' >> "$tmp/c-sectw/crates/loopd/Cargo.toml" +expect fail "role dev-depends on another role, written as a table" check-crate-deps.sh "$tmp/c-sectw" +tree "$tmp/c-dot"; manifest "$tmp/c-dot/crates/loopd" loopd 'proto.workspace = true' 'brokerd.path = "../brokerd"' +expect fail "role depends on another role, written with a dotted key" check-crate-deps.sh "$tmp/c-dot" +tree "$tmp/c-sectok"; manifest "$tmp/c-sectok/crates/loopd" loopd; printf '\n[dependencies.proto]\nworkspace = true\n' >> "$tmp/c-sectok/crates/loopd/Cargo.toml" +expect pass "role depends on proto, written as a table" check-crate-deps.sh "$tmp/c-sectok" # check-dep-docs.sh tree "$tmp/d-ok" @@ -73,6 +82,18 @@ tree "$tmp/d-prose"; printf 'rand = "0.9"\n' >> "$tmp/d-prose/Cargo.toml"; print expect fail "a mention in prose is not a table row" check-dep-docs.sh "$tmp/d-prose" tree "$tmp/d-loose"; manifest "$tmp/d-loose/crates/loopd" loopd 'proto.workspace = true' 'rand = "0.9"' expect fail "crate declares a dependency outside the workspace table" check-dep-docs.sh "$tmp/d-loose" +tree "$tmp/d-sect"; printf '\n[dependencies.rand]\nversion = "0.9"\n' >> "$tmp/d-sect/crates/loopd/Cargo.toml" +expect fail "table-form dependency without workspace = true" check-dep-docs.sh "$tmp/d-sect" +tree "$tmp/d-sectok"; printf '\n[dependencies.serde]\nworkspace = true\nfeatures = ["derive"]\n' >> "$tmp/d-sectok/crates/loopd/Cargo.toml" +expect pass "table-form dependency with workspace = true" check-dep-docs.sh "$tmp/d-sectok" + +# A check that cannot find what it checks must fail, not pass. +mkdir -p "$tmp/empty" +expect fail "check-lines without a crates directory" check-lines.sh "$tmp/empty" +expect fail "check-crate-deps without a crates directory" check-crate-deps.sh "$tmp/empty" +expect fail "check-dep-docs without a crates directory" check-dep-docs.sh "$tmp/empty" +tree "$tmp/d-nodoc"; rm "$tmp/d-nodoc/docs/dependencies.md" +expect fail "check-dep-docs without docs/dependencies.md" check-dep-docs.sh "$tmp/d-nodoc" if [ "$fails" -ne 0 ]; then echo "test-gate-scripts: $fails failure(s)" >&2 diff --git a/docs/specs/2026-09-17-pre-m1-design.md b/docs/specs/2026-09-17-pre-m1-design.md index ac1efa3..68ff6d7 100644 --- a/docs/specs/2026-09-17-pre-m1-design.md +++ b/docs/specs/2026-09-17-pre-m1-design.md @@ -111,6 +111,7 @@ pub struct Epoch(pub u32); pub struct CallId(pub u64); // assigned by loopd, increasing within a session pub struct Hash32([u8; 32]); // JSON: 64 lowercase hex chars pub struct Timestamp(u64); // Unix milliseconds; JSON: "2026-09-17T08:05:00.000Z" + // never above 9999-12-31T23:59:59.999Z (added after the M1 review) #[serde(rename_all = "lowercase")] pub enum DataClass { Public, Private, Secret } // derives PartialOrd, Ord in this order