Files
poimen-memory/tasks/M0.2-domain-types.md
T

116 lines
4.3 KiB
Markdown
Raw Normal View History

2026-08-19 09:52:07 -07:00
# M0.2 — Domain types and sha256 identity
| Field | Value |
|---|---|
| Phase | M0 — Read-only spine |
| Size | S — under 1 day |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M0.1 |
## Goal
The vocabulary every other crate speaks, and the content-hash identity the whole
provenance graph hangs on.
## Facts (inlined — no spec read needed)
```rust
/// Closed. L0 evidence, L1 per-query memory, L2 project synthesis.
#[derive(Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Level { L0, L1, L2 }
/// A normalised unit from any source. Adapters produce these; nothing
/// downstream learns whether it came from pi, claude, or a socket.
pub struct Record {
pub role: Role, // User | Assistant | ToolResult | System
pub text: String,
pub timestamp: OffsetDateTime,
pub provenance: Provenance, // source id + offset within it
}
/// One or more Records, under the token budget, never split mid-Record.
pub struct Chunk {
pub t: u32, // 1-based turn index within a run
pub records: Vec<Record>,
pub tokens: usize,
pub sha256: Sha256Hash,
}
pub struct MemoryNode {
pub level: Level,
pub project: ProjectId,
pub query_id: Option<QueryId>, // None at L2
pub run_id: RunId,
pub t: u32,
pub text: String,
pub sha256: Sha256Hash,
pub parents: Vec<Sha256Hash>,
}
```
**Identity is the content hash, not a counter.** `sha256` is computed over the
canonical serialization of the semantic content — for `Chunk`, the concatenated
record texts and their provenance; for `MemoryNode`, `(level, project, query_id,
text)`. It must **not** include the timestamp or the run id, or re-running the
same input produces different hashes and `mem rebuild` stops being idempotent.
Newtypes with no `Default`: `ProjectId`, `QueryId`, `RunId`, `Sha256Hash`. A
placeholder that type-checks is invisible — a hardcoded `"current"` compiles,
passes tests, and makes every downstream result unattributable.
## Steps
1. Declare `Level`, `Role`, `Record`, `Provenance`, `Chunk`, `MemoryNode`.
2. Declare the newtypes. None derives `Default`. None has `From<String>` without
validation.
3. `fn content_hash(&self) -> Sha256Hash` on `Chunk` and `MemoryNode`, over a
canonical byte encoding that excludes timestamps and run ids.
4. `Level` serializes as the literal strings `"L0" | "L1" | "L2"` — the JSONL and
the SQL `CHECK` constraint both depend on that spelling.
5. Round-trip serde tests for every type.
## Acceptance
- Two `Chunk`s built from identical records in different runs hash identically.
- Changing one character of any record text changes the hash.
- `Level` round-trips through JSON as `"L0"`, not `0` and not `"l0"`.
## Verify
**Harness:** unit tests in `mem-core`, plus a hash-stability fixture committed
as bytes.
**Integration test**`tests/it_identity.rs`:
1. `a1_same_content_same_hash` — build the same chunk twice with different
`RunId` and timestamps, assert equal hashes.
2. `a2_text_change_changes_hash` — flip one byte, assert the hash differs.
3. `a3_level_wire_format``serde_json::to_string(&Level::L0) == "\"L0\""`.
4. `a4_hash_stability_across_versions` — hash a committed fixture record set,
assert it equals a hash literal written into the test. This catches a
canonicalization change that would silently orphan every stored node.
5. `a5_newtypes_have_no_default` — compile-fail test (`trybuild`) asserting
`ProjectId::default()` does not compile.
**Command:** `cargo test -p mem-core identity`
**False pass:**
- Asserting only that hashing is deterministic *within one process*. A hash that
includes the timestamp is deterministic per run and still breaks rebuild.
Assertion 1 must vary the run id and timestamp deliberately.
- Omitting assertion 4. Without a committed expected hash, any future
canonicalization change passes every other test and silently invalidates the
database.
## Traps
- Including `run_id` or `created_at` in the hash. Rebuild then produces new nodes
every time and `memory_edge` accumulates orphans.
- Deriving `Default` on an id newtype "for tests". That default reaches
production and every row attributes to the same fake project.
---
Background: [DESIGN.md](../DESIGN.md) — The tier model, Storage schemas