feat(index): maintain existing indexes on re-materialization (spec §6)

TDD step 6: on an existing index, managed lines whose target is an
ingest-owned file removed this run are dropped, a managed label is
refreshed in place when the extraction title changes, and every other
line — curated links, promoted-verdict links, foreign line endings — is
preserved byte for byte. Load-bearing test: a promoted verdict and its
index link survive re-ingest unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QeqhJpYQyghASjiJo5EhGg
This commit is contained in:
Kjell Tore Guttormsen 2026-07-16 20:04:21 +02:00
commit f12fdc1dd0
2 changed files with 161 additions and 0 deletions

View file

@ -101,6 +101,36 @@ def _write_bytes(bundle_dir: Path, name: str, content: str) -> Path:
return resolved
def _update_index_lines(
index_path: Path, removed_targets: set[str], labels_by_target: dict[str, str]
) -> None:
"""§6 maintenance on an EXISTING index: drop managed lines whose target is
an ingest file removed in this run; refresh in place a managed label that
no longer equals the extraction title. Every other line is preserved
verbatim, in order including its own line ending.
"""
original = index_path.read_bytes().decode("utf-8")
lines = original.splitlines(keepends=True)
updated: list[str] = []
changed = False
for line in lines:
content = line.rstrip("\r\n")
ending = line[len(content) :]
match = _MANAGED_LINE_RE.match(content)
if match is not None:
target = match.group("target")
if target in removed_targets:
changed = True
continue
new_label = labels_by_target.get(target)
if new_label is not None and match.group("label") != new_label:
line = f"- [{new_label}]({target})" + ending
changed = True
updated.append(line)
if changed:
index_path.write_bytes("".join(updated).encode("utf-8"))
def _link_in_index(bundle_dir: Path, target_name: str, label: str) -> None:
# §6: idempotent by target — a link whose target is already present in
# the index is never added twice.
@ -215,8 +245,15 @@ def materialize_bundle(
# §6 index generation — the last disk mutation. A fresh index gets
# bundle_summary as its body; links are appended in extraction order.
index_path = bundle / _INDEX_NAME
labels_by_target = {
generated_filename(extraction.id): extraction.title for extraction in manifest.extractions
}
if not index_path.is_file():
_write_bytes(bundle, _INDEX_NAME, manifest.bundle_summary + "\n")
else:
# Links whose target is an ingest-owned file removed this run MUST be
# removed; all other links — curated and promoted — are preserved.
_update_index_lines(index_path, owned - staged_names, labels_by_target)
for extraction in manifest.extractions:
_link_in_index(bundle, generated_filename(extraction.id), extraction.title)
return IngestResult(written=written)