Files
poimen/tasks/T0.3-workevent-and-schemaversion.md
T

105 lines
4.1 KiB
Markdown

# T0.3 — `WorkEvent` and `SchemaVersion`
| Field | Value |
|---|---|
| Phase | P0 — Foundations |
| Size | M — 1 to 3 days |
| Status | ✅ Done |
| Flags | — |
| Spec | inlined below |
| Blocks | T0.4, T2.4 |
## Goal
The event enum plus the wire-version discipline that keeps a two-year-old log
decodable. Must land before the second event variant is ever written.
## Facts (inlined — no spec read needed)
```rust
pub struct LogRecord {
pub key: BranchKey, // (TenantId, RunId, BranchId)
pub lsn: Lsn,
/// Wire-format version of `event`. Never removed, never reused.
pub schema: SchemaVersion,
pub at: Timestamp,
pub event: WorkEvent,
}
pub struct BranchKey { pub tenant: TenantId, pub run: RunId, pub branch: BranchId }
```
Rules, from record one:
- Every record carries `SchemaVersion`. Written always, even at v1.
- `WorkEvent` is `#[non_exhaustive]`; decode is version-dispatched.
- **Variants are never removed or repurposed.** Deprecated variants stay
decodable forever. Storage is cheap; an undecodable log is not.
- Migrations are upcasters applied on read (T0.4), never by rewriting history.
- A round-trip test per version — a stored fixture of every historical version
still folds to the expected state. **That test is the whole guarantee**;
without it the rules above are aspirational.
- `BranchId` is in the key, not implied. LSNs are **per branch**, not global: a
global counter serializes every run through one atomic, and the ordering
contract only promises per-run total order.
## Steps
1. Declare `SchemaVersion(u16)` and `LogRecord` / `BranchKey` as above.
2. Declare `WorkEvent` as `#[non_exhaustive]` with the v1 variant set the walking
skeleton needs: attempt transitions, run-lifecycle transitions, prompt/output
blob refs, context partition, usage, intent records, and
`Reduced { original: BlobRef, summary: BlobRef }`.
3. Pick a self-describing codec (`serde` + CBOR or similar). Write
`encode(&LogRecord) -> Vec<u8>` and `decode(&[u8]) -> Result<LogRecord>`, with
decode dispatching on the `schema` field read **before** the event body.
4. Write a fixture generator that serializes one record of every variant at v1
into `tests/fixtures/v1/`. Commit the bytes.
5. Write the round-trip test: walk `tests/fixtures/*/`, decode each, assert the
expected value. A directory walk means adding a version folder automatically
extends coverage.
6. Add a test that fails if a fixture directory exists for a version with no
registered upcaster — wire this once T0.4 lands.
## Acceptance
- A serialized v1 fixture checked into the repo, plus a test that decodes it.
## Verify
**Harness:** committed fixture bytes under `tests/fixtures/v1/`, loaded from
disk. A regeneration path exists only behind an env var.
**Integration test**`tests/it_fixture_roundtrip.rs`:
1. Walk `tests/fixtures/*/`, decode every file.
2. Assert each decodes to its expected value, loaded from a committed
expectation file — not reconstructed in the test body.
3. Assert every fixture's `schema` field is present and non-zero.
4. Assert the variant coverage: every `WorkEvent` variant appears in at least one
fixture. A `match` over the decoded set with no `_` arm forces this to fail
when a variant is added without a fixture.
**Command:** `cargo test -p log fixtures` — and in CI, with
`FIXTURE_REGEN` **unset**; assert the test fails loudly if it is set.
**False pass:**
- The fixture being serialized by the code under test at test start, then
immediately decoded. That tests nothing across versions. Bytes must come from
git.
- Coverage drift: a new variant is added, no fixture is written, and the
directory walk still passes because it only checks what is there. Step 4 is the
guard.
## Traps
- Renaming one variant two years in and losing drop-and-re-fold silently.
- Non-self-describing formats where a field reorder decodes to garbage rather
than erroring.
- Omitting `schema` at v1 "because there is only one version".
---
Background (not required to do this task):
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §8.2, §8.7 ·
[rust-agentic-task.md](../../../rust-agentic-task.md)