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
|