3.9 KiB
M2.5 — Obsidian projector
| Field | Value |
|---|---|
| Phase | M2 — Projections |
| Size | M — 1–3 days |
| Status | ⬜ Not started |
| Flags | — |
| Spec | inlined below |
| Blocks | M1.6 |
Goal
Render the log as a vault a human reads, with the tier graph as the link graph.
Facts (inlined — no spec read needed)
vault/<project>/
index.md L2 synthesis, links every L1 note
<query-id>.md L1, one per standing query
evidence/<source>-<t>.md L0, only with --emit-evidence-notes
---
project: poimen
level: L1
query_id: infra-root-causes
updated: 2026-08-17
chunks_seen: 412
chunks_used: 17
run_id: 01HXYZ...
---
# Infra root causes — poimen
<final memory text, verbatim>
## Provenance
- [[pi-2026-07-21-019f857d]] chunk 66 — Kong body buffer
Deterministic output is the requirement, not a nicety. M2.8 asserts that
rebuilding produces a byte-identical vault. That means: stable key order in
frontmatter, no timestamp of generation (only updated derived from the log),
sorted provenance lists, and \n line endings.
updated comes from the run's timestamp in the log — not now(). A generation
timestamp makes every rebuild a diff and destroys the gate.
L0 notes default off: 17 per query is fine, but it grows unbounded across projects and queries. Citations inline give the same provenance without the file count.
Wikilinks are [[<source-note-name>]]. The link target may not exist as a file
when evidence notes are off — that is fine and normal in Obsidian, and it still
shows in the graph view as an unresolved node.
Steps
ObsidianProjector::project(log_dir, vault_dir, opts)inmem-store.- Read the log; take the final
memoryrecord per query for L1, and the L2 record forindex.md. - Frontmatter with a fixed key order;
updatedfrom the log. - Provenance section from
parents, sorted by source thent. --emit-evidence-noteswrites L0 notes; default off.- Write with
\n, no trailing whitespace, exactly one trailing newline. - A note whose L1 memory is empty is still written, with a body saying no evidence was found — an absent file is indistinguishable from a failed run.
Acceptance
- Two projections of the same log produce byte-identical files.
- Frontmatter key order is stable.
updatedreflects the run, not the projection.- Every L1 note links to its L2 index and vice versa.
Verify
Harness: a committed log fixture and a committed expected vault tree.
Integration test — tests/it_projector.rs:
a1_byte_identical_twice— project into two temp dirs, assert every file's bytes are equal. This is M2.8's core property, tested early.a2_no_generation_timestamp— project, sleep 1s, project again, assert equal. Catchesnow()leaking into output.a3_frontmatter_key_order— assert the exact key sequence.a4_golden_tree— diff the whole output againstexpected/vault/, empty diff.a5_empty_memory_still_writes— a log with zero updates produces a note saying so.a6_links_bidirectional— every L1 note appears inindex.mdand links back.a7_evidence_notes_flag— off by default; on, produces one note per L0 node.a8_line_endings— no\r, exactly one trailing\n.
Command: cargo test -p mem-store projector
False pass:
- Comparing files by parsed content rather than bytes. Key reordering and whitespace churn both pass, and both fail M2.8 later, where the cause is much harder to find.
- Testing with a single query. Assertion 6 needs at least two L1 notes to catch a link built from the wrong id.
Traps
updated: {now}. The most natural thing to write, and it makes every rebuild dirty, which trains everyone to ignore the diff that M2.8 depends on.- Serializing frontmatter from a
HashMap. Iteration order is unspecified and the output churns between runs on the same input.
Background: DESIGN.md — Obsidian vault