2026-07-09-openwiki-output-snapshot-plan.md
docs/superpowers/plans/2026-07-09-openwiki-output-snapshot-plan.md
Compile Output Snapshot Hash Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax.
Goal: After every ProjectWiki.compile, hash the byte-idempotent compiled artifact set and report output_changed / output_sha256 / idempotence_suspect in the compile result dict, print it from the CLI, gate compile --strict on the suspect flag, and persist the hashes to a deterministic .tesserae/output-snapshot.json state file so downstream automation (vault sync, CI refresh PRs, agents) gets a machine-readable no-op signal. This is OpenWiki's snapshot-gate pattern (createOpenWikiContentSnapshot in src/agent/utils.ts: SHA-256 the output dir excluding its own metadata file; write metadata only when the snapshot changed) repurposed as (a) a no-op detector and (b) a permanent byte-idempotence tripwire — this repo's compile determinism broke 4× via wall-clock/mutable state, and tests only catch drift over the fixture corpus; the tripwire watches every real compile.
Architecture: One new stdlib-only module tesserae/output_snapshot.py computes a two-part snapshot: a graph layer hash (config.json + graph.json + code-graph.json + combined-graph.json — the exact inputs the projections are a pure function of) and a projections layer hash (wiki/, site/, markdown_projection/, minus the append-only ledgers). Project.compile snapshots the tree before ingest and after the post-compile lint, inside the existing compile_lock. Comparing before/after within one run needs no input fingerprint and has zero false positives:
output_changed= any part differs → the no-op signal (OpenWiki-equivalent).idempotence_suspect= graph layer unchanged but projections layer changed → this compile rewrote a projection of identical inputs differently, i.e. a deterministic-projection violation. Sound because every projector under_write_artifactsderiveswiki//site//markdown_projection/purely from (canonicalized graph, config).
Graph-layer nondeterminism (the historical 4×: wall-clock in graph.json) cannot be auto-proven at runtime without fingerprinting every input (docs + sessions + vault edits + LLM caches — rejected as over-engineering with false-positive risk); it becomes observable instead: a no-op recompile logs/prints output: changed, and a cron CI refresh produces visible junk PRs immediately, which is exactly how OpenWiki's gate surfaces the same failure.
Tech Stack: Python 3 stdlib (hashlib, json, dataclasses, pathlib), pytest. No new dependencies.
Global Constraints
- Byte-idempotence audit — every disk write this plan adds:
.tesserae/output-snapshot.json— the ONLY new artifact. Lives at the.tesserae/root (likevault_snapshot.json/manifest.json), is excluded from the hash by construction (the hash scope is an allowlist that never includes it), contains no timestamps and no wall-clock state (only hex digests + a bool), is serialized withsort_keys=True, indent=2+ trailing"\n"(mirroringBatchIngestRunner._write_manifest), and is written via tmp-file +os.replace. Two compiles over identical inputs write byte-identical state files. Existing idempotence tests hash onlysite/,wiki/,graph.json,temporal_facts.jsonl→ unaffected.- Nothing else touches disk. Log lines go to
logger/stdout only. The CI example is a doc. - Hash scope is an ALLOWLIST of test-proven-stable artifacts only (see
tests/test_idempotence.py::test_compile_is_byte_idempotentand the phase-5 suite). Deliberately excluded for v1 because their byte-stability is unproven and one noisy artifact would make the signal cry wolf:report.md,competitive_report.md,temporal_facts.jsonl(depends on the mutablenode_memorysidecar viamemory_by_id— MCP reads bumpaccess_countbetween compiles),cognee_bundle/,graphiti_episodes.jsonl,agent_harness/,sqlite.db, the Obsidian vault (bidirectional, user-owned),manifest.json(input state), lint reports, and all ledgers/caches. Extending scope later is a one-line allowlist edit. - Excluded basenames inside allowlisted dirs:
.history.jsonl(synthesis ledger insidewiki/syntheses/, seesynthesis.py:654),.build-history.jsonl(defensive),.DS_Store— same exclusionstest_idempotence._hash_treealready uses. config.jsonbelongs to the graph layer, not the projections layer:site_title/namefeedKarpathyLayerWriterand the site build, so a config-only change must read as "inputs changed" (no suspect flag), not as projection drift.- The tripwire never fails a default compile —
idempotence_suspectis a result key +logger.warning; onlycompile --strictturns it into exit 2 (consistent with strict's existing lint gate atcli.py:1287). - New result-dict keys are additive; no existing key changes.
step_compileintesserae refreshneeds no changes. - Snapshot cost is two tree walks of already-written files — no LLM, no network.
- Run tests with
.venv/bin/python -m pytest(system python3 fails collection). - One commit per task; conventional messages.
File Structure
- Create
tesserae/output_snapshot.py—OutputSnapshotdataclass,snapshot_output(root),write_state(path, snapshot, changed), allowlist constants. - Modify
tesserae/project.py—ProjectPaths.output_snapshotfield; before/after snapshot calls + result keys + warning log + state write inProjectWiki.compile. - Modify
tesserae/cli.py—_handle_compile: print the output line;--strictgains the suspect gate. - Modify
tests/test_idempotence.py— snapshot unit tests + compile-integration tests (reuses_seed_project,_deterministic_compile,_hash_tree). - Modify
tests/test_cli_commands.py— CLI print + strict-gate tests. - Create
docs/integrations/ci-refresh.md— optional shipped CI workflow example (mirrors OpenWikiexamples/openwiki-update.yml).
Task 1: tesserae/output_snapshot.py
Files: Create tesserae/output_snapshot.py; Test tests/test_idempotence.py
Interfaces:
GRAPH_LAYER_FILES: tuple[str, ...] = (
"config.json", "graph.json", "code-graph.json", "combined-graph.json",
)
PROJECTION_LAYER_DIRS: tuple[str, ...] = ("wiki", "site", "markdown_projection")
EXCLUDED_BASENAMES: frozenset[str] = frozenset(
{".history.jsonl", ".build-history.jsonl", ".DS_Store"}
)
@dataclass(frozen=True)
class OutputSnapshot:
graph_sha256: str
projections_sha256: str
@property
def output_sha256(self) -> str:
# sha256 over the two part-digests — the single combined hash.
...
def snapshot_output(root: Path) -> OutputSnapshot: ...
def write_state(path: Path, snapshot: OutputSnapshot, changed: bool) -> None: ...
Hashing rules (all deterministic, platform-stable):
- Graph layer: for each name in
GRAPH_LAYER_FILESin tuple order, feedname + "\0"then the file bytes into onehashlib.sha256; a missing file feeds the literal sentinelb"missing"(mirrors OpenWiki'saddDirectoryToSnapshotrace handling). A deletedcombined-graph.json(config flip) therefore changes the graph hash — correct, it is an output change. - Projections layer: for each dir in
PROJECTION_LAYER_DIRS, walksorted(dir.rglob("*")), skip non-files andEXCLUDED_BASENAMES, feed forward-slashedrelative_to(root)path +"\0"+ bytes; a missing dir feedsb"missing". write_statewrites{"changed": bool, "graph_sha256": ..., "output_sha256": ..., "projections_sha256": ...}withsort_keys=True, indent=2+"\n", viapath.with_suffix(".tmp")+os.replace.- [ ] Step 1: Write the failing tests (append to
tests/test_idempotence.py):
def test_snapshot_output_stable_and_ignores_ledgers_and_state(tmp_path: Path) -> None:
from tesserae.output_snapshot import snapshot_output, write_state
wiki = _seed_project(tmp_path / "proj")
wiki.compile(session_options=SessionExtractionOptions(enabled=False))
root = wiki.root
first = snapshot_output(root)
# Excluded churn: ledgers, state file, lint noise at the root.
with (root / ".build-history.jsonl").open("a") as fh:
fh.write('{"noise": true}\n')
(root / "wiki" / "syntheses").mkdir(parents=True, exist_ok=True)
with (root / "wiki" / "syntheses" / ".history.jsonl").open("a") as fh:
fh.write('{"noise": true}\n')
write_state(root / "output-snapshot.json", first, changed=False)
assert snapshot_output(root) == first
# Included churn: a projection file flips only the projections part.
(root / "wiki" / "drift.md").write_text("drift", encoding="utf-8")
second = snapshot_output(root)
assert second.projections_sha256 != first.projections_sha256
assert second.graph_sha256 == first.graph_sha256
assert second.output_sha256 != first.output_sha256
def test_snapshot_output_handles_missing_artifacts(tmp_path: Path) -> None:
from tesserae.output_snapshot import snapshot_output
empty = snapshot_output(tmp_path / "nothing-here")
assert empty == snapshot_output(tmp_path / "nothing-here") # deterministic
assert len(empty.graph_sha256) == 64 and len(empty.projections_sha256) == 64
- [ ] Step 2: Run to verify failure —
.venv/bin/python -m pytest tests/test_idempotence.py -k snapshot_output -v→ FAIL (ModuleNotFoundError). - [ ] Step 3: Implement
tesserae/output_snapshot.pyper the interface above. Module docstring must state the allowlist rationale and point at this plan. - [ ] Step 4: Re-run → PASS. Commit:
feat(compile): output snapshot hashing module
Task 2: Hook into ProjectWiki.compile
Files: Modify tesserae/project.py; Test tests/test_idempotence.py
Interfaces:
ProjectPathsgainsoutput_snapshot: Path = Path(".tesserae/output-snapshot.json")(append afterextraction_guidance_cache— keeps positional compatibility, same pattern assession_findings), andProjectWiki.__init__passesoutput_snapshot=self.root / "output-snapshot.json"explicitly.compile()result dict gains:output_sha256: str,output_changed: bool,idempotence_suspect: bool.
Placement inside compile() (project.py:1421), all within the existing compile_lock block:
- Immediately after entering
compile_lock, beforeself.ingest(...):before = snapshot_output(self.root). - After the existing post-compile lint try/except (lint writes only root-level reports — outside the allowlist, so ordering is cosmetic; taking the snapshot last keeps the bracket honest):
after = snapshot_output(self.root)
result["output_sha256"] = after.output_sha256
result["output_changed"] = after != before
result["idempotence_suspect"] = (
after.graph_sha256 == before.graph_sha256
and after.projections_sha256 != before.projections_sha256
)
if result["idempotence_suspect"]:
logger.warning(
"byte-idempotence regression suspected: projections changed while "
"graph.json/config were byte-identical (before=%s after=%s)",
before.projections_sha256[:12], after.projections_sha256[:12],
)
else:
logger.info(
"compile output %s (sha256 %s)",
"changed" if result["output_changed"] else "unchanged",
after.output_sha256[:12],
)
try:
write_state(self.paths.output_snapshot, after, changed=result["output_changed"])
except OSError:
logger.exception("output-snapshot state write failed; compile artifacts are unaffected")
Notes:
- First-ever compile:
beforehashes all-missing sentinels →output_changed=True,graphpart differs → suspect stays False. No special-casing. - New sessions / vault edits / config flips / LLM-minted
resolved_byedges all mutate the graph layer → suspect never fires on legitimate input change. - Import
snapshot_output, write_stateat module top (stdlib-only, no cycle:output_snapshotimports nothing fromtesserae). - [ ] Step 1: Write the failing tests (append to
tests/test_idempotence.py; reuse_seed_project+ the autouse_deterministic_compilefixture; passsession_options=SessionExtractionOptions(enabled=False)exactly liketest_compile_is_byte_idempotentdoes):
def test_compile_result_reports_output_unchanged_on_recompile(tmp_path: Path) -> None:
wiki = _seed_project(tmp_path / "proj")
opts = SessionExtractionOptions(enabled=False)
first = wiki.compile(session_options=opts)
second = wiki.compile(session_options=opts)
assert first["output_changed"] is True # first compile populated an empty tree
assert second["output_changed"] is False # no-op detected
assert second["idempotence_suspect"] is False
assert second["output_sha256"] == first["output_sha256"]
def test_compile_result_reports_output_changed_on_new_source(tmp_path: Path) -> None:
wiki = _seed_project(tmp_path / "proj")
opts = SessionExtractionOptions(enabled=False)
first = wiki.compile(session_options=opts)
(tmp_path / "proj" / "docs" / "new-note.md").write_text(
"# New Note\n\nA fresh concept: snapshot gating.\n", encoding="utf-8"
)
second = wiki.compile(session_options=opts)
assert second["output_changed"] is True
assert second["output_sha256"] != first["output_sha256"]
assert second["idempotence_suspect"] is False # graph layer changed too
def test_compile_flags_idempotence_suspect_on_projection_drift(
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Simulate the historical failure class: a projector emitting bytes not
derived from the graph. Graph layer identical + projections drifted must
raise the tripwire."""
from tesserae.karpathy_layer import KarpathyLayerWriter
wiki = _seed_project(tmp_path / "proj")
opts = SessionExtractionOptions(enabled=False)
wiki.compile(session_options=opts)
original = KarpathyLayerWriter.write_all
def drifting(self, graph, build_history_path=None):
written = original(self, graph, build_history_path)
(Path(self.wiki_root) / "drift.md").write_text("wall-clock leak", encoding="utf-8")
return written
monkeypatch.setattr(KarpathyLayerWriter, "write_all", drifting)
second = wiki.compile(session_options=opts)
assert second["idempotence_suspect"] is True
assert second["output_changed"] is True
def test_output_snapshot_state_file_is_byte_stable(tmp_path: Path) -> None:
wiki = _seed_project(tmp_path / "proj")
opts = SessionExtractionOptions(enabled=False)
wiki.compile(session_options=opts)
state_path = wiki.paths.output_snapshot
assert state_path.exists()
first_bytes = state_path.read_bytes()
payload = json.loads(first_bytes)
assert set(payload) == {"changed", "graph_sha256", "output_sha256", "projections_sha256"}
wiki.compile(session_options=opts)
second = json.loads(state_path.read_bytes())
assert second["changed"] is False
# Identical hashes both runs; only `changed` may differ on the first run.
assert second["output_sha256"] == payload["output_sha256"]
(Signature verified: write_all(self, graph: ResearchGraph, build_history_path: Optional[Path]) -> List[Path] at tesserae/karpathy_layer.py:194; wiki_root is an attribute.)
- [ ] Step 2: Run to verify failure —
.venv/bin/python -m pytest tests/test_idempotence.py -k "output_unchanged or output_changed_on_new or projection_drift or state_file" -v→ FAIL (KeyErroroutput_changed). - [ ] Step 3: Implement the
ProjectPathsfield +compile()hook as specified. - [ ] Step 4: Re-run the new tests AND the full existing idempotence suites —
.venv/bin/python -m pytest tests/test_idempotence.py tests/test_byte_idempotence_phase5.py -v→ all PASS (proves the state-file write breaks nothing). Commit:feat(compile): output-changed no-op signal + idempotence tripwire
Task 3: CLI surface — print line + --strict gate
Files: Modify tesserae/cli.py (_handle_compile, around lines 1282–1310); Test tests/test_cli_commands.py
Interfaces:
- After the existing
print(f"Graph: {result['graph_path']}"), add one stable, script-parseable line:Output: unchanged (sha256 <first-12>)orOutput: changed (sha256 <first-12>)— keyed offresult.get("output_changed"); omit the line entirely when the key is absent (defensive: injected compile doubles in older tests). --strictgains a gate BEFORE the lint checks (a suspected determinism regression outranks lint warnings):
if getattr(args, "strict", False):
if result.get("idempotence_suspect"):
print(
"compile --strict: projections changed while graph/config were "
"byte-identical — byte-idempotence regression suspected",
file=sys.stderr,
)
return 2
...existing lint gate unchanged...
- The
--stricthelp string is updated to mention both gates. - [ ] Step 1: Write the failing tests (append to
tests/test_cli_commands.py, following thetest_compile_options_flow_into_wiki_compilemonkeypatch pattern — stubProjectWiki.compileto return a canned result dict includinglint,output_changed,output_sha256,idempotence_suspect): test_compile_prints_output_change_line— canned result withoutput_changed=False→ stdout containsOutput: unchanged; withoutput_changed=True→Output: changed.test_compile_strict_fails_on_idempotence_suspect— canned result withidempotence_suspect=True, zero lint counts,--strict→ exit code 2 and stderr mentionsbyte-idempotence.test_compile_strict_passes_when_output_clean— suspect False, zero lint counts,--strict→ exit 0.- [ ] Step 2: Run to verify failure —
.venv/bin/python -m pytest tests/test_cli_commands.py -k "output_change_line or strict_fails_on_idempotence or strict_passes_when_output_clean" -v→ FAIL. - [ ] Step 3: Implement, re-run → PASS. Also re-run the whole
tests/test_cli_commands.py(the canned-result stubs elsewhere must not regress). Commit:feat(cli): compile prints output-change line; --strict gates on idempotence tripwire
Task 4 (optional): shipped CI refresh workflow example
Files: Create docs/integrations/ci-refresh.md
Mirror OpenWiki's examples/openwiki-update.yml (daily cron 0 8 * * * + workflow_dispatch, peter-evans/create-pull-request), adapted to Tesserae. The doc contains a short intro plus one fenced YAML block:
- Steps: checkout (
persist-credentials: true) →actions/setup-python3.11 →pip install tesserae→tesserae compile --project .→ a gate step reading the state file this plan ships:if [ "$(jq -r .changed .tesserae/output-snapshot.json)" != "true" ]; then echo "no-op"; fiexposed as a step output →peter-evans/create-pull-request@v7withadd-paths: .tesserae/wiki,branch: tesserae/refresh,commit-message: "docs: refresh tesserae knowledge base", run only when the gate output says changed. - Call out explicitly in the doc: the
changedgate is what prevents endless scheduled-PR loops (the OpenWiki lesson), and a PR appearing when nothing changed in the repo is the live symptom of a byte-idempotence regression — file it as a bug. - No test (docs-only); keep the YAML under ~60 lines.
- [ ] Step 1: Write
docs/integrations/ci-refresh.md. - [ ] Step 2: Sanity-check the YAML with
python3 -c "import yaml,sys; yaml.safe_load(open('...'))"if PyYAML is available in.venv, else eyeball. Commit:docs(integrations): scheduled CI refresh workflow example
Verification
.venv/bin/python -m pytest tests/test_idempotence.py tests/test_byte_idempotence_phase5.py tests/test_cli_commands.py -v— all green.- Manual:
cdinto a real registered project, runtesserae compiletwice; second run must printOutput: unchanged (sha256 …)and.tesserae/output-snapshot.jsonmust be byte-identical across the two runs (shasumit). If it printschanged, that is the tripwire doing its job — investigate before shipping.