105 lines
4.1 KiB
Markdown
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)
|