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

4.3 KiB

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)

/// 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 Chunks 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 testtests/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_formatserde_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 — The tier model, Storage schemas