114 lines
3.9 KiB
Markdown
114 lines
3.9 KiB
Markdown
# 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
|
||
```
|
||
|
||
```markdown
|
||
---
|
||
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
|
||
|
||
1. `ObsidianProjector::project(log_dir, vault_dir, opts)` in `mem-store`.
|
||
2. Read the log; take the final `memory` record per query for L1, and the L2
|
||
record for `index.md`.
|
||
3. Frontmatter with a fixed key order; `updated` from the log.
|
||
4. Provenance section from `parents`, sorted by source then `t`.
|
||
5. `--emit-evidence-notes` writes L0 notes; default off.
|
||
6. Write with `\n`, no trailing whitespace, exactly one trailing newline.
|
||
7. 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.
|
||
- `updated` reflects 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`:
|
||
1. `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.
|
||
2. `a2_no_generation_timestamp` — project, sleep 1s, project again, assert equal.
|
||
Catches `now()` leaking into output.
|
||
3. `a3_frontmatter_key_order` — assert the exact key sequence.
|
||
4. `a4_golden_tree` — diff the whole output against `expected/vault/`, empty diff.
|
||
5. `a5_empty_memory_still_writes` — a log with zero updates produces a note
|
||
saying so.
|
||
6. `a6_links_bidirectional` — every L1 note appears in `index.md` and links back.
|
||
7. `a7_evidence_notes_flag` — off by default; on, produces one note per L0 node.
|
||
8. `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](../DESIGN.md) — Obsidian vault
|