fix(materialize): scope §5 replacement to the running manifest's stem
The §3 ownership scan classified every ingest-stamped file as replaceable,
so materializing one manifest into a bundle deleted the ingest files another
manifest had stamped there — and removed their index links too.
Narrow ownership to files whose stamp names the running manifest by stem.
The stamp is `{stem}@{sha256[:16]}`; matching on the stem, not the whole
stamp, lets an edited manifest still reclaim the files a prior run of the
same manifest wrote (the stem is stable across content edits), while a
different manifest sharing the bundle keeps its own files. Implements the
trinn-e §10.2 decision. This does not close the operator-copy restriction
(a generated file copied into curated content), which stays documented in
ingest-spec.md:69-72, not enforced.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBbjgS5A55RVavoyjJC4FX
This commit is contained in:
parent
f14c075a65
commit
1747238a83
2 changed files with 96 additions and 4 deletions
|
|
@ -81,12 +81,25 @@ def _parse_frontmatter(path: Path) -> dict[str, str]:
|
|||
return frontmatter
|
||||
|
||||
|
||||
def _is_ingest_owned(path: Path) -> bool:
|
||||
def _is_ingest_owned(path: Path, manifest_stem: str) -> bool:
|
||||
# §3/§5 ownership: the ingest stamp is `generated: true` AND an
|
||||
# `ingest_manifest` reference. Promoted verdict files carry neither key,
|
||||
# so they can never classify as ingest-owned.
|
||||
#
|
||||
# §10.2 per-manifest ownership: a file is THIS manifest's to replace only
|
||||
# when the reference names it by stem. The stamp is `{stem}@{sha256[:16]}`;
|
||||
# matching on the stem — not the whole stamp — lets an edited manifest (new
|
||||
# content -> new sha -> new stamp) still reclaim the files a prior run of
|
||||
# the same manifest wrote, while a DIFFERENT manifest sharing the bundle
|
||||
# keeps its own. rsplit strips the trailing `@{sha}`, so a stem that itself
|
||||
# contains `@` still compares correctly.
|
||||
frontmatter = _parse_frontmatter(path)
|
||||
return frontmatter.get("generated") == "true" and "ingest_manifest" in frontmatter
|
||||
if frontmatter.get("generated") != "true":
|
||||
return False
|
||||
reference = frontmatter.get("ingest_manifest")
|
||||
if reference is None:
|
||||
return False
|
||||
return reference.rsplit("@", 1)[0] == manifest_stem
|
||||
|
||||
|
||||
def _render_concept_file(
|
||||
|
|
@ -255,7 +268,7 @@ def materialize_bundle(
|
|||
owned = {
|
||||
path.name
|
||||
for path in sorted(bundle.glob("*.md"))
|
||||
if path.name != _INDEX_NAME and _is_ingest_owned(path)
|
||||
if path.name != _INDEX_NAME and _is_ingest_owned(path, manifest_file.stem)
|
||||
}
|
||||
# §3 collision gate — BEFORE any mutation: a staged filename occupied by
|
||||
# a file WITHOUT the stamp is curated content; never overwrite it.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue