93 lines
3.9 KiB
Markdown
93 lines
3.9 KiB
Markdown
# T0.8 — Fold and re-derive
|
||||
|
|
|
|||
|
|
| Field | Value |
|
|||
|
|
|---|---|
|
|||
|
|
| Phase | P0 — Foundations |
|
|||
|
|
| Size | M — 1 to 3 days |
|
|||
|
|
| Status | Not started |
|
|||
|
|
| Flags | — |
|
|||
|
|
| Spec | inlined below |
|
|||
|
|
| Blocks | P1 gate |
|
|||
|
|
|
|||
|
|
## Goal
|
|||
|
|
|
|||
|
|
The projection from log to materialized state, as a pure function. This task
|
|||
|
|
establishes the property every later phase re-runs.
|
|||
|
|
|
|||
|
|
## Facts (inlined — no spec read needed)
|
|||
|
|
|
|||
|
|
- **Nothing derived is authoritative.** Materialized state is a cache of the log.
|
|||
|
|
If it cannot be dropped and rebuilt byte-identically, it has hidden inputs and
|
|||
|
|
that is a bug.
|
|||
|
|
- The fold is pure: `fold(state, record) -> state`. No clock reads, no random,
|
|||
|
|
no filesystem, no map iteration order leaking into the output.
|
|||
|
|
- Reduction (T8.4) is designed around this property: it appends a
|
|||
|
|
`Reduced { original, summary }` event rather than editing the log or rewriting
|
|||
|
|
a blob, precisely so the fold still reaches the same state from LSN 0.
|
|||
|
|
- Restart folds forward from the newest checkpoint (T2.5); `None` means fold from
|
|||
|
|
LSN 0. Checkpoints are an optimization only — deleting every checkpoint costs
|
|||
|
|
startup time and nothing else.
|
|||
|
|
|
|||
|
|
## Steps
|
|||
|
|
|
|||
|
|
1. Define the materialized state type: runs, branches, attempts, refs, intent
|
|||
|
|
status. Whatever the query surface (T1.7) needs and nothing more.
|
|||
|
|
2. Write `fn fold(state: &mut State, record: &LogRecord)` as a pure function.
|
|||
|
|
Take no `&self`, no store handle, no `Instant::now()`.
|
|||
|
|
3. Use ordered collections (`BTreeMap`) anywhere the state is serialized, so
|
|||
|
|
byte-identity does not depend on hash seed.
|
|||
|
|
4. Write `rebuild(log, key) -> State`: read from LSN 0 (or from the newest
|
|||
|
|
checkpoint once T2.5 lands), fold every record in order.
|
|||
|
|
5. Write the property test as a **reusable helper**, not a one-off: drop the
|
|||
|
|
state tables, `rebuild`, assert the serialized bytes match. Every subsequent
|
|||
|
|
phase calls this helper on its own fixtures.
|
|||
|
|
6. Wire that helper into P1–P8 fixture suites as they land.
|
|||
|
|
|
|||
|
|
## Acceptance
|
|||
|
|
|
|||
|
|
- Drop the state tables, re-fold from LSN 0, assert byte-identical state.
|
|||
|
|
- The helper is exported and used by at least one later phase's fixture set.
|
|||
|
|
|
|||
|
|
## Verify
|
|||
|
|
|
|||
|
|
**Harness:** export `assert_refold_identical(&log, key)` as a **public test
|
|||
|
|
helper from this crate**. Every later phase calls it on its own fixtures; that
|
|||
|
|
reuse is the deliverable, not the one-off test.
|
|||
|
|
|
|||
|
|
**Integration test** — `tests/it_refold.rs`:
|
|||
|
|
1. Build a log containing a retry, a rewind (once T2.3 lands) and a reduction
|
|||
|
|
(once T8.4 lands) — the three cases with non-trivial fold logic.
|
|||
|
|
2. Snapshot `serialize(state)`.
|
|||
|
|
3. Drop the state tables entirely.
|
|||
|
|
4. `rebuild` from LSN 0.
|
|||
|
|
5. Assert **byte equality** of the serialized state, not `PartialEq`.
|
|||
|
|
6. Repeat the fold twice in the same process and assert both runs produce
|
|||
|
|
identical bytes — catches hash-seed and iteration-order leakage.
|
|||
|
|
7. Purity check: run the fold with the system clock moved forward and with a
|
|||
|
|
different `TZ`; assert the bytes are unchanged.
|
|||
|
|
|
|||
|
|
**Command:** `cargo test -p projection refold`
|
|||
|
|
|
|||
|
|
**False pass:**
|
|||
|
|
- Comparing states with derived `PartialEq`. `HashMap` compares order-insensitively,
|
|||
|
|
so byte-identity can be broken while the test is green — and byte-identity is
|
|||
|
|
the actual property.
|
|||
|
|
- Testing on a log with one linear run and no branches, retries or reductions.
|
|||
|
|
Every implementation passes that.
|
|||
|
|
- Running the fold once and comparing to the live state that was built by the
|
|||
|
|
same code path incrementally — assert against a committed expected snapshot for
|
|||
|
|
at least one fixture.
|
|||
|
|
|
|||
|
|
## Traps
|
|||
|
|
|
|||
|
|
- `HashMap` in serialized state — iteration order varies and byte-identity fails
|
|||
|
|
intermittently, which reads as flakiness rather than as the bug it is.
|
|||
|
|
- Any hidden input: a timestamp taken at fold time, a config value read from the
|
|||
|
|
environment, a default filled in from the current schema version.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
Background (not required to do this task):
|
|||
|
|
[rust-agentic-sys.md](../../../rust-agentic-sys.md) §1, §8.6 ·
|
|||
|
|
[rust-agentic-task.md](../../../rust-agentic-task.md)
|