',
+ f"{html.escape(cast(str, page['title']))}
",
+ '',
+ f"- ID
- {html.escape(node_id)}
",
+ f"- Family
- {html.escape(cast(str, page['family']))}
",
+ f"- Status
- {html.escape(cast(str, page['status']))}
",
+ f"- Authority
- {html.escape(cast(str, page['authority']))}
",
+ "
",
+ f'{html.escape(cast(str, page["summary"]))}
',
+ self.markdown.render(cast(str, page["content"])).rstrip(),
+ ]
+ )
+ relationships = cast(list[object], page["cross_references"])
+ if relationships:
+ sections.append('')
+ for relationship_value in relationships:
+ relationship = cast(dict[str, object], relationship_value)
+ sections.append(
+ f"- {html.escape(cast(str, relationship['relation']))}: "
+ f"{html.escape(cast(str, relationship['target_id']))}
"
+ )
+ sections.append("
")
+ sections.append("")
+ return "\n".join(sections)
diff --git a/src/docforge_renderers/py.typed b/src/docforge_renderers/py.typed
new file mode 100644
index 0000000..e75b43f
--- /dev/null
+++ b/src/docforge_renderers/py.typed
@@ -0,0 +1 @@
+# PEP 561 marker for the typed DocForge renderer package.
diff --git a/tests/test_graph_projection.py b/tests/test_graph_projection.py
new file mode 100644
index 0000000..d0b516a
--- /dev/null
+++ b/tests/test_graph_projection.py
@@ -0,0 +1,404 @@
+from __future__ import annotations
+
+import json
+import tempfile
+import unittest
+from dataclasses import replace
+from pathlib import Path
+from typing import Any, cast
+
+from docforge.errors import DocForgeError
+from docforge.graph_projection import (
+ GraphViewRequestV1,
+ build_graph_view_plan,
+)
+from docforge.models import Edge, Limits, Node, ProjectDescriptor, ProjectSnapshot
+from docforge.projection_contract import GraphViewPlanV1
+
+
+def _document(plan: GraphViewPlanV1) -> dict[str, Any]:
+ return cast(dict[str, Any], plan.as_dict())
+
+
+def _node(
+ node_id: str,
+ *,
+ title: str | None = None,
+ family: str = "code",
+ authority: str = "derived",
+ status: str = "active",
+ tags: tuple[str, ...] = (),
+) -> Node:
+ return Node(
+ node_id=node_id,
+ title=title or node_id,
+ family=family,
+ authority=authority,
+ status=status,
+ tags=tags,
+ summary=f"Summary for {node_id}",
+ content=f"SECRET SOURCE BODY {node_id}",
+ source_path=f"/private/source/{node_id}.py",
+ source_anchor=f"line-{len(node_id)}",
+ content_hash=(node_id.encode("utf-8").hex() + "0" * 64)[:64],
+ )
+
+
+def _snapshot(
+ root: Path,
+ nodes: tuple[Node, ...],
+ edges: tuple[Edge, ...],
+) -> ProjectSnapshot:
+ descriptor = ProjectDescriptor(
+ schema_version=1,
+ project_id="graph-project",
+ title="Graph project",
+ adapter="generic",
+ root=root,
+ descriptor_path=root / ".docforge" / "project.toml",
+ descriptor_hash="d" * 64,
+ content_roots=(root / "docs",),
+ authority_files=(),
+ cache_root=root / ".docforge" / "cache",
+ index_path=root / ".docforge" / "cache" / "index.sqlite3",
+ changeset_root=root / ".docforge" / "changesets",
+ proposal_writers=(),
+ render=None,
+ allowed_relations=tuple(sorted({edge.relation for edge in edges})),
+ profiles=(),
+ limits=Limits(),
+ )
+ return ProjectSnapshot(
+ descriptor=descriptor,
+ nodes=nodes,
+ edges=edges,
+ revision="revision-1",
+ source_hash="a" * 64,
+ )
+
+
+class GraphProjectionTests(unittest.TestCase):
+ def test_exact_root_plan_is_deterministic_sorted_and_path_free(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ nodes = (
+ _node("c", family="docs", tags=("python",)),
+ _node("a", tags=("python", "callable")),
+ _node("b", tags=("python",)),
+ _node("unrelated"),
+ )
+ edges = (
+ Edge("b", "calls", "c"),
+ Edge("a", "calls", "b"),
+ Edge("unrelated", "calls", "c"),
+ )
+ request = GraphViewRequestV1(
+ view_id="architecture",
+ title="Architecture",
+ root_node_id="a",
+ depth=2,
+ max_nodes=10,
+ max_edges=10,
+ max_work=100,
+ )
+ first = build_graph_view_plan(_snapshot(root, nodes, edges), request, True)
+ second = build_graph_view_plan(
+ _snapshot(root, tuple(reversed(nodes)), tuple(reversed(edges))),
+ request,
+ True,
+ )
+ self.assertEqual(first.as_dict(), second.as_dict())
+ GraphViewPlanV1.from_dict(first.as_dict())
+ document = _document(first)
+ self.assertEqual(
+ ["a", "b", "c"], [node["node_id"] for node in document["graph"]["nodes"]]
+ )
+ self.assertEqual(
+ [
+ {"source_id": "a", "relation": "calls", "target_id": "b"},
+ {"source_id": "b", "relation": "calls", "target_id": "c"},
+ ],
+ document["graph"]["edges"],
+ )
+ encoded = json.dumps(document, sort_keys=True)
+ self.assertNotIn(str(root), encoded)
+ self.assertNotIn("SECRET SOURCE BODY", encoded)
+ self.assertNotIn("/private/source/", encoded)
+ self.assertNotIn("line-1", encoded)
+ self.assertEqual("excluded", document["policy"]["source_paths"])
+ self.assertEqual("excluded", document["policy"]["source_bodies"])
+ self.assertEqual("allowed", document["policy"]["logic"])
+ self.assertEqual("exact_root", document["view"]["scope"]["kind"])
+
+ def test_lexical_scope_uses_metadata_only_and_closed_filters(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ nodes = (
+ _node(
+ "api.handler",
+ title="Request handler",
+ family="code",
+ authority="derived",
+ tags=("python", "route"),
+ ),
+ _node(
+ "api.test",
+ title="Handler proof",
+ family="test",
+ authority="approved_plan",
+ tags=("python", "test"),
+ ),
+ replace(
+ _node("hidden.body", family="code", tags=("python",)),
+ content="request handler appears only in the forbidden source body",
+ ),
+ )
+ edges = (
+ Edge("api.handler", "tested_by", "api.test"),
+ Edge("hidden.body", "relates_to", "api.handler"),
+ )
+ request = GraphViewRequestV1(
+ view_id="routes",
+ title="Routes",
+ query="request handler",
+ families=("code",),
+ authorities=("derived",),
+ tags=("python", "route"),
+ relations=("tested_by",),
+ max_nodes=10,
+ max_edges=10,
+ max_work=100,
+ )
+ plan = build_graph_view_plan(_snapshot(root, nodes, edges), request, False)
+ document = _document(plan)
+ self.assertEqual(
+ ["api.handler"],
+ [node["node_id"] for node in document["graph"]["nodes"]],
+ )
+ self.assertEqual([], document["graph"]["edges"])
+ self.assertEqual(
+ {
+ "families": ["code"],
+ "relations": ["tested_by"],
+ "authorities": ["derived"],
+ "statuses": [],
+ "tags": ["python", "route"],
+ },
+ document["view"]["filters"],
+ )
+ self.assertEqual("lexical", document["view"]["scope"]["kind"])
+
+ def test_result_and_work_limits_emit_explicit_omissions(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ nodes = tuple(_node(value) for value in ("a", "b", "c", "d"))
+ edges = (
+ Edge("a", "calls", "b"),
+ Edge("a", "calls", "c"),
+ Edge("a", "calls", "d"),
+ Edge("b", "calls", "c"),
+ )
+ result_limited = build_graph_view_plan(
+ _snapshot(root, nodes, edges),
+ GraphViewRequestV1(
+ view_id="limited",
+ title="Limited",
+ root_node_id="a",
+ max_nodes=2,
+ max_edges=0,
+ max_work=100,
+ ),
+ False,
+ )
+ result_limited = _document(result_limited)
+ self.assertEqual(
+ ["a", "b"], [node["node_id"] for node in result_limited["graph"]["nodes"]]
+ )
+ self.assertEqual([], result_limited["graph"]["edges"])
+ self.assertEqual(
+ ["edge_result_limit", "node_result_limit"],
+ [item["code"] for item in result_limited["omissions"]],
+ )
+
+ work_limited = build_graph_view_plan(
+ _snapshot(root, nodes, edges),
+ GraphViewRequestV1(
+ view_id="work",
+ title="Work",
+ root_node_id="a",
+ max_nodes=10,
+ max_edges=10,
+ max_work=1,
+ ),
+ False,
+ )
+ work_limited = _document(work_limited)
+ self.assertIn(
+ "work_limit",
+ [item["code"] for item in work_limited["omissions"]],
+ )
+ self.assertEqual(
+ 1,
+ work_limited["diagnostics"]["examined_work_units"],
+ )
+ self.assertTrue(work_limited["diagnostics"]["truncated"])
+
+ def test_no_ast_policy_excludes_requested_logic(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ snapshot = _snapshot(root, (_node("a"),), ())
+ request = GraphViewRequestV1(
+ view_id="logic",
+ title="Logic",
+ root_node_id="a",
+ initial_mode="logic",
+ include_logic=True,
+ )
+ blocked = _document(build_graph_view_plan(snapshot, request, False))
+ self.assertEqual("forbidden", blocked["policy"]["logic"])
+ self.assertEqual([], blocked["graph"]["logic_projections"])
+ self.assertIn(
+ "logic_forbidden",
+ [item["code"] for item in blocked["omissions"]],
+ )
+ allowed = _document(build_graph_view_plan(snapshot, request, True))
+ self.assertEqual("allowed", allowed["policy"]["logic"])
+ self.assertNotIn(
+ "logic_forbidden",
+ [item["code"] for item in allowed["omissions"]],
+ )
+
+ def test_edge_and_node_filters_constrain_exact_root_bfs(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ nodes = (
+ _node("root", family="code", status="active"),
+ _node("code-child", family="code", status="active"),
+ _node("doc-child", family="docs", status="active"),
+ _node("old-child", family="code", status="historical"),
+ )
+ edges = (
+ Edge("root", "calls", "code-child"),
+ Edge("root", "documents", "doc-child"),
+ Edge("root", "calls", "old-child"),
+ )
+ plan = build_graph_view_plan(
+ _snapshot(root, nodes, edges),
+ GraphViewRequestV1(
+ view_id="filtered",
+ title="Filtered",
+ root_node_id="root",
+ families=("code",),
+ statuses=("active",),
+ relations=("calls",),
+ max_work=100,
+ ),
+ False,
+ )
+ plan = _document(plan)
+ self.assertEqual(
+ ["code-child", "root"], [node["node_id"] for node in plan["graph"]["nodes"]]
+ )
+ self.assertEqual(
+ [{"source_id": "root", "relation": "calls", "target_id": "code-child"}],
+ plan["graph"]["edges"],
+ )
+
+ def test_invalid_requests_and_graphs_fail_closed(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ snapshot = _snapshot(root, (_node("a"),), ())
+ invalid_requests = (
+ GraphViewRequestV1(view_id="v", title="V"),
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="a",
+ query="a",
+ ),
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="a",
+ max_nodes=0,
+ ),
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ query="***",
+ ),
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="a",
+ families=("code", "code"),
+ ),
+ )
+ for request in invalid_requests:
+ with self.subTest(request=request), self.assertRaises(DocForgeError) as error:
+ build_graph_view_plan(snapshot, request, False)
+ self.assertEqual("invalid_graph_view_request", error.exception.code)
+
+ with self.assertRaises(DocForgeError) as missing:
+ build_graph_view_plan(
+ snapshot,
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="missing",
+ ),
+ False,
+ )
+ self.assertEqual("missing_node", missing.exception.code)
+
+ duplicate = _snapshot(root, (_node("a"), _node("a")), ())
+ with self.assertRaises(DocForgeError) as invalid:
+ build_graph_view_plan(
+ duplicate,
+ GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="a",
+ ),
+ False,
+ )
+ self.assertEqual("invalid_projection", invalid.exception.code)
+
+ def test_plan_identity_changes_with_generation_request_and_policy(self) -> None:
+ with tempfile.TemporaryDirectory() as directory:
+ root = Path(directory)
+ snapshot = _snapshot(root, (_node("a"),), ())
+ request = GraphViewRequestV1(
+ view_id="v",
+ title="V",
+ root_node_id="a",
+ )
+ base = build_graph_view_plan(snapshot, request, False)
+ self.assertEqual(
+ base.plan_id,
+ build_graph_view_plan(snapshot, request, False).plan_id,
+ )
+ self.assertNotEqual(
+ base.plan_id,
+ build_graph_view_plan(
+ replace(snapshot, source_hash="b" * 64),
+ request,
+ False,
+ ).plan_id,
+ )
+ self.assertNotEqual(
+ base.plan_id,
+ build_graph_view_plan(
+ snapshot,
+ replace(request, title="Other"),
+ False,
+ ).plan_id,
+ )
+ self.assertNotEqual(
+ base.plan_id,
+ build_graph_view_plan(snapshot, request, True).plan_id,
+ )
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_projection_contract.py b/tests/test_projection_contract.py
new file mode 100644
index 0000000..882527a
--- /dev/null
+++ b/tests/test_projection_contract.py
@@ -0,0 +1,486 @@
+from __future__ import annotations
+
+import copy
+import hashlib
+import json
+import os
+import shutil
+import sqlite3
+import tempfile
+import unittest
+from dataclasses import replace
+from pathlib import Path
+from unittest import mock
+
+from docforge.errors import DocForgeError
+from docforge.manual_projection import (
+ build_manual_projection_package,
+ build_manual_render_plan,
+)
+from docforge.models import Edge
+from docforge.project import Project
+from docforge.projection_contract import (
+ MANUAL_RENDER_PLAN_CONTRACT,
+ PROJECTION_PACKAGE_CONTRACT,
+ PROJECTION_RECEIPT_CONTRACT,
+ ManualRenderPlanV1,
+ ProjectionArtifact,
+ ProjectionPackageV1,
+ ProjectionReceiptV1,
+ canonical_projection_bytes,
+ projection_hash,
+)
+from docforge.render_contract import GenericHtmlRenderer
+from docforge_renderers.manual import ManualHtmlRenderer
+
+ROOT = Path(__file__).resolve().parents[1]
+FIXTURES = ROOT / "tests" / "fixtures"
+
+ALPHA_RENDERER_VERSION = "1+markdown-it-py-4.2.0"
+ALPHA_RENDER_IDENTITY = "1c0a49c28ba3b0dabf94be36e75def197dee1be3cb73ac405b09875383c8dc5f"
+ALPHA_OUTPUT_HASH = "81656bb89debc7ad1fbe8bc290e9a3ba90664442b17a6d57e908d30d20c47f77"
+ALPHA_OUTPUT_BYTES = 2043
+
+
+class ProjectionContractTests(unittest.TestCase):
+ def setUp(self) -> None:
+ self.temporary = tempfile.TemporaryDirectory()
+ self.addCleanup(self.temporary.cleanup)
+ self.root = Path(self.temporary.name) / "alpha"
+ shutil.copytree(FIXTURES / "alpha", self.root)
+ self.project = Project.open(self.root)
+ self.snapshot = self.project.load()
+ assert self.snapshot.descriptor.render is not None
+ self.view = self.snapshot.descriptor.render.views[0]
+ self.template = self.view.template_path.read_bytes()
+ self.plan = build_manual_render_plan(
+ self.snapshot,
+ self.view,
+ changeset_hash=None,
+ )
+ self.package = build_manual_projection_package(
+ self.plan,
+ self.template,
+ renderer_id="generic_html",
+ renderer_version=ALPHA_RENDERER_VERSION,
+ max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
+ )
+
+ def test_canonical_identity_is_stable_and_tampering_is_rejected(self) -> None:
+ self.assertEqual(
+ b'{"a":"\xc3\xa9","b":1}',
+ canonical_projection_bytes({"b": 1, "a": "\N{LATIN SMALL LETTER E WITH ACUTE}"}),
+ )
+ self.assertEqual(
+ projection_hash(
+ {key: value for key, value in self.plan.as_dict().items() if key != "plan_id"}
+ ),
+ self.plan.plan_id,
+ )
+ self.assertEqual(
+ self.plan.plan_id,
+ build_manual_render_plan(
+ self.snapshot,
+ self.view,
+ changeset_hash=None,
+ ).plan_id,
+ )
+ self.assertEqual(
+ self.package.package_id,
+ ProjectionPackageV1.from_dict(
+ json.loads(json.dumps(self.package.as_dict()))
+ ).package_id,
+ )
+
+ tampered_plan = copy.deepcopy(self.plan.as_dict())
+ pages = tampered_plan["pages"]
+ assert isinstance(pages, list)
+ assert isinstance(pages[0], dict)
+ pages[0]["title"] = "Tampered title"
+ with self.assertRaises(DocForgeError) as plan_error:
+ ManualRenderPlanV1.from_dict(tampered_plan)
+ self.assertEqual("invalid_projection", plan_error.exception.code)
+
+ tampered_package = copy.deepcopy(self.package.as_dict())
+ assets = tampered_package["assets"]
+ assert isinstance(assets, list)
+ assert isinstance(assets[0], dict)
+ assets[0]["text"] = f"{assets[0]['text']}\nTampered"
+ with self.assertRaises(DocForgeError) as package_error:
+ ProjectionPackageV1.from_dict(tampered_package)
+ self.assertEqual("invalid_projection", package_error.exception.code)
+
+ def test_contract_documents_reject_unknown_or_malformed_fields(self) -> None:
+ plan_with_extra = copy.deepcopy(self.plan.as_dict())
+ plan_with_extra["unexpected"] = True
+ with self.assertRaises(DocForgeError) as extra_plan:
+ ManualRenderPlanV1.from_dict(plan_with_extra)
+ self.assertEqual("invalid_projection", extra_plan.exception.code)
+
+ plan_with_foreign_project = copy.deepcopy(self.plan.as_dict())
+ project = plan_with_foreign_project["project"]
+ assert isinstance(project, dict)
+ project["absolute_root"] = str(self.root)
+ with self.assertRaises(DocForgeError) as foreign_project:
+ ManualRenderPlanV1.from_dict(plan_with_foreign_project)
+ self.assertEqual("invalid_projection", foreign_project.exception.code)
+
+ package_with_extra = copy.deepcopy(self.package.as_dict())
+ package_with_extra["unexpected"] = []
+ with self.assertRaises(DocForgeError) as extra_package:
+ ProjectionPackageV1.from_dict(package_with_extra)
+ self.assertEqual("invalid_projection", extra_package.exception.code)
+
+ result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(self.package)
+ receipt_with_extra = copy.deepcopy(result.receipt.as_dict())
+ receipt_with_extra["artifact_bytes"] = "forbidden"
+ with self.assertRaises(DocForgeError) as extra_receipt:
+ ProjectionReceiptV1.from_dict(receipt_with_extra)
+ self.assertEqual("invalid_projection", extra_receipt.exception.code)
+
+ with self.assertRaises(DocForgeError) as path_artifact:
+ ProjectionReceiptV1.create(
+ kind="manual",
+ package_id=self.package.package_id,
+ plan_id=self.plan.plan_id,
+ renderer={
+ "renderer_id": "generic_html",
+ "renderer_version": ALPHA_RENDERER_VERSION,
+ },
+ artifacts=[
+ {
+ "artifact_id": "../manual.html",
+ "media_type": "text/html",
+ "sha256": "0" * 64,
+ "bytes": 1,
+ }
+ ],
+ diagnostics={},
+ timing={"elapsed_ns": 0},
+ peak_memory_bytes=None,
+ )
+ self.assertEqual("invalid_projection", path_artifact.exception.code)
+
+ def test_projection_package_is_path_free_and_rejects_runtime_references(self) -> None:
+ serialized = canonical_projection_bytes(self.package.as_dict())
+ self.assertNotIn(str(self.root).encode("utf-8"), serialized)
+ self.assertNotIn(b"source_path", serialized)
+ self.assertNotIn(b"sqlite", serialized.lower())
+
+ with self.assertRaises(DocForgeError) as absolute_path:
+ ProjectionPackageV1.create(
+ kind="manual",
+ plan=self.plan,
+ renderer={
+ "renderer_id": "generic_html",
+ "renderer_version": ALPHA_RENDERER_VERSION,
+ },
+ components=[],
+ assets=[],
+ output_policy={
+ "artifact_ids": ["manual.html"],
+ "max_total_bytes": 1000,
+ "template_path": "/home/example/private-template.html",
+ },
+ )
+ self.assertEqual("invalid_projection", absolute_path.exception.code)
+
+ with self.assertRaises(DocForgeError) as database_reference:
+ ProjectionPackageV1.create(
+ kind="manual",
+ plan=self.plan,
+ renderer={
+ "renderer_id": "generic_html",
+ "renderer_version": ALPHA_RENDERER_VERSION,
+ },
+ components=[],
+ assets=[],
+ output_policy={
+ "artifact_ids": ["manual.html"],
+ "max_total_bytes": 1000,
+ "database": "index.sqlite3",
+ },
+ )
+ self.assertEqual("invalid_projection", database_reference.exception.code)
+
+ def test_receipt_attests_artifacts_without_embedding_content(self) -> None:
+ result = ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(
+ self.package,
+ render_identity=ALPHA_RENDER_IDENTITY,
+ )
+ self.assertEqual(1, len(result.artifacts))
+ artifact = result.artifacts[0]
+ evidence = artifact.evidence()
+ receipt = result.receipt.as_dict()
+
+ self.assertEqual(self.package.package_id, receipt["package_id"])
+ self.assertEqual(self.plan.plan_id, receipt["plan_id"])
+ self.assertEqual([evidence], receipt["artifacts"])
+ self.assertEqual(PROJECTION_RECEIPT_CONTRACT, receipt["contract"])
+ self.assertEqual(
+ {
+ "renderer_id": "generic_html",
+ "renderer_version": ALPHA_RENDERER_VERSION,
+ },
+ receipt["renderer"],
+ )
+ self.assertEqual({"warnings": []}, receipt["diagnostics"])
+ self.assertIsNone(receipt["peak_memory_bytes"])
+ timing = receipt["timing"]
+ assert isinstance(timing, dict)
+ self.assertGreaterEqual(timing["elapsed_ns"], 0)
+ self.assertNotIn("content", evidence)
+ self.assertNotIn(artifact.content, canonical_projection_bytes(receipt))
+ self.assertEqual(
+ receipt["receipt_id"],
+ ProjectionReceiptV1.from_dict(copy.deepcopy(receipt)).receipt_id,
+ )
+
+ tampered_receipt = copy.deepcopy(receipt)
+ artifacts = tampered_receipt["artifacts"]
+ assert isinstance(artifacts, list)
+ assert isinstance(artifacts[0], dict)
+ artifacts[0]["bytes"] = int(artifacts[0]["bytes"]) + 1
+ with self.assertRaises(DocForgeError) as tampered:
+ ProjectionReceiptV1.from_dict(tampered_receipt)
+ self.assertEqual("invalid_projection", tampered.exception.code)
+
+ def test_manual_plan_is_deterministic_and_preserves_alpha_semantics(self) -> None:
+ document = self.plan.as_dict()
+ self.assertEqual(MANUAL_RENDER_PLAN_CONTRACT, document["contract"])
+ self.assertIsNone(document["changeset_hash"])
+ pages = document["pages"]
+ navigation = document["navigation"]
+ search_documents = document["search_documents"]
+ diagnostics = document["diagnostics"]
+ assert isinstance(pages, list)
+ assert isinstance(navigation, list)
+ assert isinstance(search_documents, list)
+ assert isinstance(diagnostics, dict)
+
+ self.assertEqual(
+ ["guide.foundation", "guide.workflow", "proof.validation"],
+ [page["node_id"] for page in pages],
+ )
+ self.assertEqual(
+ ["guide.foundation", "guide.workflow", "proof.validation"],
+ [item["node_id"] for item in navigation],
+ )
+ self.assertEqual(
+ ["guide.foundation", "guide.workflow", "proof.validation"],
+ [item["node_id"] for item in search_documents],
+ )
+ self.assertEqual([], diagnostics["orphans"])
+ self.assertEqual([], diagnostics["cycles"])
+
+ page_by_id = {page["node_id"]: page for page in pages}
+ self.assertEqual(
+ [
+ {
+ "source_id": "guide.workflow",
+ "relation": "depends_on",
+ "target_id": "guide.foundation",
+ }
+ ],
+ page_by_id["guide.foundation"]["backlinks"],
+ )
+ self.assertEqual(
+ [
+ {
+ "source_id": "guide.workflow",
+ "relation": "depends_on",
+ "target_id": "guide.foundation",
+ }
+ ],
+ page_by_id["guide.workflow"]["cross_references"],
+ )
+ self.assertEqual(
+ [
+ {
+ "source_id": "proof.validation",
+ "relation": "proves",
+ "target_id": "guide.workflow",
+ }
+ ],
+ page_by_id["guide.workflow"]["backlinks"],
+ )
+ self.assertEqual(
+ [
+ {
+ "source_id": "proof.validation",
+ "relation": "proves",
+ "target_id": "guide.workflow",
+ }
+ ],
+ page_by_id["proof.validation"]["cross_references"],
+ )
+ self.assertTrue(
+ all(
+ page["components"]
+ == [
+ "manual.node-metadata@1",
+ "manual.summary@1",
+ "manual.commonmark@1",
+ "manual.relationships@1",
+ ]
+ for page in pages
+ )
+ )
+
+ proposed = build_manual_render_plan(
+ self.snapshot,
+ self.view,
+ changeset_hash="a" * 64,
+ )
+ self.assertNotEqual(self.plan.plan_id, proposed.plan_id)
+ self.assertEqual("a" * 64, proposed.as_dict()["changeset_hash"])
+
+ def test_cycle_orphan_backlink_and_cross_reference_planning(self) -> None:
+ edges = (
+ Edge("guide.foundation", "relates_to", "guide.workflow"),
+ Edge("guide.workflow", "returns_to", "guide.foundation"),
+ )
+ snapshot = replace(self.snapshot, edges=edges)
+ first = build_manual_render_plan(snapshot, self.view, changeset_hash=None)
+ second = build_manual_render_plan(snapshot, self.view, changeset_hash=None)
+ self.assertEqual(first.plan_id, second.plan_id)
+
+ document = first.as_dict()
+ diagnostics = document["diagnostics"]
+ pages = document["pages"]
+ assert isinstance(diagnostics, dict)
+ assert isinstance(pages, list)
+ self.assertEqual(["proof.validation"], diagnostics["orphans"])
+ self.assertEqual(
+ [["guide.foundation", "guide.workflow"]],
+ diagnostics["cycles"],
+ )
+
+ page_by_id = {page["node_id"]: page for page in pages}
+ foundation = page_by_id["guide.foundation"]
+ workflow = page_by_id["guide.workflow"]
+ self.assertEqual(
+ [
+ {
+ "source_id": "guide.foundation",
+ "relation": "relates_to",
+ "target_id": "guide.workflow",
+ }
+ ],
+ foundation["cross_references"],
+ )
+ self.assertEqual(
+ [
+ {
+ "source_id": "guide.workflow",
+ "relation": "returns_to",
+ "target_id": "guide.foundation",
+ }
+ ],
+ foundation["backlinks"],
+ )
+ self.assertEqual(
+ foundation["cross_references"],
+ workflow["backlinks"],
+ )
+ self.assertEqual(
+ foundation["backlinks"],
+ workflow["cross_references"],
+ )
+
+ def test_alpha_compatibility_shim_preserves_legacy_identity_and_bytes(self) -> None:
+ renderer = GenericHtmlRenderer()
+ self.assertEqual(ALPHA_RENDERER_VERSION, renderer.renderer_version)
+ prepared = renderer.prepare(
+ self.snapshot,
+ self.view,
+ self.template,
+ changeset_hash=None,
+ )
+
+ self.assertEqual(ALPHA_RENDER_IDENTITY, prepared.render_identity)
+ self.assertEqual(ALPHA_OUTPUT_HASH, prepared.output_hash)
+ self.assertEqual(ALPHA_OUTPUT_BYTES, len(prepared.output))
+ self.assertEqual(
+ ALPHA_OUTPUT_HASH,
+ hashlib.sha256(prepared.output).hexdigest(),
+ )
+ self.assertEqual(b"", prepared.output.splitlines()[0])
+ self.assertTrue(prepared.output.endswith(b"