Files

114 lines
3.9 KiB
Markdown
Raw Permalink Normal View History

2026-08-19 09:52:07 -07:00
# M2.5 — Obsidian projector
| Field | Value |
|---|---|
| Phase | M2 — Projections |
| Size | M — 13 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