3.4 KiB
3.4 KiB
T0.4 — Upcaster framework
| Field | Value |
|---|---|
| Phase | P0 — Foundations |
| Size | M — 1 to 3 days |
| Status | Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | T2.4 |
Goal
Read-time migration chain so old log records fold correctly after the event enum changes. Never rewrite stored records.
Facts (inlined — no spec read needed)
- Migrations are upcasters:
fn upcast(vN) -> vN+1, applied on read. - Rewriting an append-only log is a contradiction. History stays as written; the reader adapts.
- Registry keyed by version. Reading a v1 record under a v3 binary runs
v1→v2→v3in sequence. - Deprecated variants stay decodable forever — an upcaster maps them forward, it does not delete them.
- The guarantee is the fixture test: a stored fixture of every historical version still folds to the expected state.
Steps
- Keep the per-version wire types as real types —
WorkEventV1,WorkEventV2, … — with the current one aliased toWorkEvent. - Define the upcaster shape:
fn(vN) -> vN+1, either as function pointers or atrait Upcast { type From; type To; fn upcast(from: Self::From) -> Self::To; }. - Build the registry keyed by
SchemaVersion. Registration is explicit; a missing link is an error at registry construction, not at read time. read_record(bytes): decodeschema, decode the matchingvNbody, then apply each upcaster in order up toCURRENT_SCHEMA.- Return typed errors —
UnknownSchemaVersion,MissingUpcaster { from, to }. A corrupt or future-version record must not panic the fold. - Build the synthetic v1→v2 migration as the test vehicle: add a field or split
a variant in
WorkEventV2, write the upcaster, commit fixtures for both.
Acceptance
- Synthetic v1 → v2 migration with a fixture per version.
- Folding a v1 log through the upcaster yields the same state as a natively-v2 log. Assert on the folded state, not on intermediate events.
Verify
Harness: committed v1 and v2 fixtures, plus T0.8's fold.
Integration test — tests/it_upcast_equivalence.rs:
- Fold the committed v1 log through the upcaster chain →
state_a. - Fold the committed native v2 log directly →
state_b. - Assert
serialize(state_a) == serialize(state_b)byte for byte. - Instrument the registry with a counter; assert at least one upcaster actually ran on the v1 path.
- Feed a record stamped with an unknown future version; assert
UnknownSchemaVersionis returned and no panic escapes.
Command: cargo test -p log upcast
False pass:
CURRENT_SCHEMAstill equal to v1, so the chain is empty and both folds trivially agree. Step 4 is the guard.- Comparing folded states with a derived
PartialEqoverHashMap, which ignores ordering. Compare serialized bytes. - The v2 change being cosmetic (a renamed local, an added comment) so no upcaster logic is exercised. The v2 fixture must contain a record whose shape actually differs.
Traps
- Lossy upcasters that default a new field to a value the fold treats as
meaningful. If v1 cannot supply the field, the fold handles
Option— it does not receive a fabricated value. - A registry populated by iteration order rather than an explicit chain. The gap only shows at v3.
Background (not required to do this task): rust-agentic-sys.md §8.7 · rust-agentic-task.md