Preserve served immutable revision across unchanged source rebuilds
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 1m41s
Some checks failed
Build and publish policy-nexus image / build-and-push (push) Failing after 1m41s
Assistant: codex Assistant-Model: gpt-6.1-sol Assistant-Session: 01a126e1-76f8-7643-8cf3-e93336539ade
This commit is contained in:
parent
5c41564f04
commit
6d2c7eef77
3 changed files with 66 additions and 6 deletions
|
|
@ -573,7 +573,17 @@
|
|||
"revision_path": "standards/security-layer-model/v0.7/revisions/{revision}/index.html",
|
||||
"review_interval": "3m",
|
||||
"lifecycle": "superseded",
|
||||
"successor": "/standards/security-layer-model/v0.8/"
|
||||
"successor": "/standards/security-layer-model/v0.8/",
|
||||
"historical_revisions": [
|
||||
{
|
||||
"revision": "0.7",
|
||||
"source_path": "docs/evidence/2026-10-11-layer-v07-immutable-publication/security-layer-model-v0.7.md",
|
||||
"source_digest": "8155e7b123be1e84377d8278525b1bad8961007b6bb8ff012dd01ba24ff8065b",
|
||||
"source_revision": "ce198fc2905687ea90a2892346b6860281ac87f8",
|
||||
"rendered_page_path": "docs/evidence/2026-10-11-layer-v07-immutable-publication/security-layer-model-v0.7.html",
|
||||
"rendered_page_digest": "7d7a38a8c3b9fff65b46280fcd5723eff77dc9d0784f871bb35f718bc4e8c0e7"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "netkingdom-security-layer-model-v0.8",
|
||||
|
|
|
|||
|
|
@ -321,6 +321,53 @@ class HistoricalRevisionTest(unittest.TestCase):
|
|||
self.assertEqual("Original Founder", meta["decided_by"])
|
||||
self.assertEqual("2026-08-18", meta["date"])
|
||||
|
||||
def test_same_current_frozen_render_survives_new_head_and_superseded_notice(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest, source, page, original = self.prepare(root)
|
||||
source.write_bytes(page.with_suffix(".md").read_bytes())
|
||||
data = json.loads(manifest.read_text())
|
||||
data["documents"][0].update(lifecycle="superseded", successor="/standards/example/v2/")
|
||||
manifest.write_text(json.dumps(data))
|
||||
output = root / "clean"
|
||||
with mock.patch.object(build_site, "_source_revision", return_value="b" * 40):
|
||||
build_site.build(manifest, output, as_of=dt.date(2026, 8, 18))
|
||||
frozen = output / "standards/example/v1/revisions/draft-1/index.html"
|
||||
current = output / "standards/example/v1/index.html"
|
||||
self.assertEqual(original, frozen.read_bytes())
|
||||
self.assertIn('policy-source-revision" content="' + "a" * 40, frozen.read_text())
|
||||
self.assertNotIn("Superseded.", frozen.read_text())
|
||||
self.assertIn("Superseded.", current.read_text())
|
||||
self.assertIn('policy-source-revision" content="' + "b" * 40, current.read_text())
|
||||
self.assertEqual(source.read_bytes(), page.with_suffix(".md").read_bytes())
|
||||
|
||||
def test_same_current_frozen_render_rejects_current_source_change(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest, source, page, _original = self.prepare(root)
|
||||
source.write_bytes(page.with_suffix(".md").read_bytes() + b"Changed current bytes.")
|
||||
with self.assertRaisesRegex(RuntimeError, "same-current historical source differs"):
|
||||
build_site.build(manifest, root / "clean")
|
||||
self.assertFalse((root / "clean").exists())
|
||||
|
||||
def test_frozen_current_revision_accepts_source_version_frontmatter(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
manifest, source, page, original = self.prepare(root)
|
||||
history = page.with_suffix(".md")
|
||||
history.write_bytes(history.read_bytes().replace(b'revision: "draft-1"', b'version: "draft-1"'))
|
||||
source.write_bytes(history.read_bytes())
|
||||
data = json.loads(manifest.read_text())
|
||||
historical = data["documents"][0]["historical_revisions"][0]
|
||||
previous_digest = historical["source_digest"]
|
||||
historical["source_digest"] = hashlib.sha256(history.read_bytes()).hexdigest()
|
||||
original = original.replace(previous_digest.encode(), historical["source_digest"].encode())
|
||||
page.write_bytes(original)
|
||||
historical["rendered_page_digest"] = hashlib.sha256(original).hexdigest()
|
||||
manifest.write_text(json.dumps(data))
|
||||
build_site.build(manifest, root / "clean")
|
||||
self.assertEqual(original, (root / "clean/standards/example/v1/revisions/draft-1/index.html").read_bytes())
|
||||
|
||||
def test_changed_historical_page_fails_before_replacing_output(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
|
|
|
|||
|
|
@ -123,12 +123,11 @@ def load_manifest(path: Path) -> dict[str, Any]:
|
|||
|
||||
def _restore_historical_revisions(
|
||||
document: dict[str, Any], repo: Path, output: Path, current_revision: str,
|
||||
current_source_digest: str,
|
||||
) -> None:
|
||||
"""Retain explicitly pinned served revision bytes even in a clean build."""
|
||||
for historical in document.get("historical_revisions", []):
|
||||
revision = historical["revision"]
|
||||
if revision == current_revision:
|
||||
raise ValueError("historical revision must differ from current revision")
|
||||
source = (repo / historical["source_path"]).resolve()
|
||||
page = (repo / historical["rendered_page_path"]).resolve()
|
||||
for artifact in (source, page):
|
||||
|
|
@ -137,10 +136,14 @@ def _restore_historical_revisions(
|
|||
source_bytes, page_bytes = source.read_bytes(), page.read_bytes()
|
||||
if hashlib.sha256(source_bytes).hexdigest() != historical["source_digest"]:
|
||||
raise RuntimeError("historical source digest mismatch")
|
||||
# A served revision can still be the document's current content revision.
|
||||
# Freeze its old render/provenance only if today's source bytes are exact.
|
||||
if revision == current_revision and historical["source_digest"] != current_source_digest:
|
||||
raise RuntimeError("same-current historical source differs from current source")
|
||||
if hashlib.sha256(page_bytes).hexdigest() != historical["rendered_page_digest"]:
|
||||
raise RuntimeError("historical rendered page digest mismatch")
|
||||
meta, _ = split_frontmatter(source_bytes.decode("utf-8"))
|
||||
if meta.get("id") != document["id"] or meta.get("revision") != revision:
|
||||
if meta.get("id") != document["id"] or (meta.get("revision") or meta.get("version")) != revision:
|
||||
raise ValueError("historical source id/revision mismatch")
|
||||
page_text = page_bytes.decode("utf-8")
|
||||
source_revision = SOURCE_REVISION.search(page_text)
|
||||
|
|
@ -254,9 +257,9 @@ def build(
|
|||
revision = meta.get("revision") or meta.get("version")
|
||||
if not revision:
|
||||
raise ValueError(f"{source}: revision or version is required")
|
||||
_restore_historical_revisions(document, repo, temporary, revision)
|
||||
source_revision = _source_revision(repo, source)
|
||||
source_digest = hashlib.sha256(source.read_bytes()).hexdigest()
|
||||
_restore_historical_revisions(document, repo, temporary, revision, source_digest)
|
||||
source_revision = _source_revision(repo, source)
|
||||
reviewed = meta.get("last_reviewed") or meta.get("updated")
|
||||
interval = document.get("review_interval") or meta.get("review_interval")
|
||||
if not reviewed or not interval:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue