1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Add versioned independent projection contracts

This commit is contained in:
Andraxion 2026-07-29 10:50:05 -04:00
parent 4c5773c865
commit 96e3965855
22 changed files with 3561 additions and 133 deletions

View file

@ -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()

View file

@ -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"<!DOCTYPE html>", prepared.output.splitlines()[0])
self.assertTrue(prepared.output.endswith(b"</html>\n"))
self.assertIn(
f'content="{ALPHA_RENDER_IDENTITY}"'.encode(),
prepared.output,
)
def test_manual_renderer_rejects_project_provided_active_content(self) -> None:
for active in (
"<script>alert(1)</script>{{ docforge_content }}",
'<main onclick="alert(1)">{{ docforge_content }}</main>',
'<a href="javascript:alert(1)">{{ docforge_content }}</a>',
'<iframe src="https://example.invalid"></iframe>{{ docforge_content }}',
'<meta http-equiv="refresh" content="0">{{ docforge_content }}',
):
with self.subTest(active=active):
package = build_manual_projection_package(
self.plan,
active.encode("utf-8"),
renderer_id="generic_html",
renderer_version=ALPHA_RENDERER_VERSION,
max_output_bytes=self.snapshot.descriptor.limits.max_render_bytes,
)
with self.assertRaises(DocForgeError) as rejected:
ManualHtmlRenderer(ALPHA_RENDERER_VERSION).render(package)
self.assertEqual("invalid_template", rejected.exception.code)
def test_manual_renderer_has_no_project_sqlite_or_path_write_capability(self) -> None:
renderer = ManualHtmlRenderer(ALPHA_RENDERER_VERSION)
forbidden = AssertionError("manual renderer crossed its capability boundary")
with (
mock.patch.object(Project, "open", side_effect=forbidden),
mock.patch.object(Project, "load", side_effect=forbidden),
mock.patch.object(sqlite3, "connect", side_effect=forbidden),
mock.patch.object(Path, "write_bytes", side_effect=forbidden),
mock.patch.object(Path, "write_text", side_effect=forbidden),
mock.patch.object(Path, "mkdir", side_effect=forbidden),
mock.patch.object(Path, "touch", side_effect=forbidden),
mock.patch.object(Path, "unlink", side_effect=forbidden),
mock.patch.object(Path, "rename", side_effect=forbidden),
mock.patch.object(Path, "replace", side_effect=forbidden),
mock.patch.object(os, "mkdir", side_effect=forbidden),
mock.patch.object(os, "makedirs", side_effect=forbidden),
mock.patch.object(os, "rename", side_effect=forbidden),
mock.patch.object(os, "replace", side_effect=forbidden),
mock.patch.object(os, "unlink", side_effect=forbidden),
):
result = renderer.render(
self.package,
render_identity=ALPHA_RENDER_IDENTITY,
)
self.assertEqual(1, len(result.artifacts))
self.assertEqual("manual.html", result.artifacts[0].artifact_id)
self.assertEqual(ALPHA_OUTPUT_HASH, result.artifacts[0].evidence()["sha256"])
def test_projection_artifact_evidence_is_canonical_and_content_free(self) -> None:
artifact = ProjectionArtifact(
artifact_id="manual.html",
media_type="text/html; charset=utf-8",
content=b"manual bytes",
)
self.assertEqual(
{
"artifact_id": "manual.html",
"media_type": "text/html; charset=utf-8",
"sha256": hashlib.sha256(b"manual bytes").hexdigest(),
"bytes": len(b"manual bytes"),
},
artifact.evidence(),
)
self.assertEqual(
PROJECTION_PACKAGE_CONTRACT,
self.package.as_dict()["contract"],
)
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,314 @@
from __future__ import annotations
import copy
import json
import unittest
from pathlib import Path
from jsonschema import Draft202012Validator
from docforge.errors import DocForgeError
from docforge.projection_contract import (
GraphViewPlanV1,
ManualRenderPlanV1,
ProjectionPackageV1,
ProjectionReceiptV1,
)
ROOT = Path(__file__).resolve().parents[1]
SCHEMAS = ROOT / "schemas"
class ProjectionSchemaTests(unittest.TestCase):
@staticmethod
def schema(name: str) -> dict[str, object]:
return json.loads((SCHEMAS / name).read_text(encoding="utf-8"))
@classmethod
def validator(cls, name: str) -> Draft202012Validator:
return Draft202012Validator(cls.schema(name))
@staticmethod
def project_identity() -> dict[str, object]:
return {
"project_id": "schema-fixture",
"project_root_fingerprint": "a" * 16,
"adapter": "generic",
"revision": "fixture-revision",
"source_hash": "b" * 64,
}
@classmethod
def manual_plan(cls) -> ManualRenderPlanV1:
return ManualRenderPlanV1.create(
{
"project": cls.project_identity(),
"view": {
"view_id": "manual",
"title": "Schema Manual",
"families": ["guide"],
"renderer": "generic_html",
},
"changeset_hash": None,
"pages": [
{
"node_id": "guide.schema",
"title": "Projection schema",
"family": "guide",
"authority": "authoritative",
"status": "approved",
"tags": ["schema"],
"summary": "Defines the projection schema fixture.",
"content": "The schema fixture is deterministic.",
"content_hash": "c" * 64,
"components": ["manual.commonmark@1"],
"breadcrumbs": [],
"cross_references": [],
"backlinks": [],
}
],
"navigation": [
{
"node_id": "guide.schema",
"title": "Projection schema",
}
],
"search_documents": [
{
"node_id": "guide.schema",
"title": "Projection schema",
"summary": "Defines the projection schema fixture.",
"family": "guide",
"status": "approved",
"tags": ["schema"],
}
],
"diagnostics": {"orphans": ["guide.schema"], "cycles": []},
}
)
@classmethod
def graph_plan(cls) -> GraphViewPlanV1:
return GraphViewPlanV1.create(
{
"project": cls.project_identity(),
"view": {
"view_id": "portable",
"title": "Portable graph",
"initial_mode": "nodes",
"scope": {
"kind": "exact_root",
"root_node_id": "guide.schema",
"depth": 2,
},
"filters": {
"families": [],
"relations": [],
"authorities": [],
"statuses": [],
"tags": [],
},
"detail_fields": [
"node_id",
"title",
"family",
"authority",
"status",
"tags",
"summary",
"content_hash",
],
},
"bounds": {
"depth": 2,
"max_nodes": 100,
"max_edges": 400,
"max_work": 100000,
},
"policy": {
"visibility": "selected_graph_only",
"source_paths": "excluded",
"source_bodies": "excluded",
"database_queries": "forbidden",
"executable_content": "forbidden",
"logic": "forbidden",
"logic_requested": False,
},
"graph": {
"root_node_id": "guide.schema",
"nodes": [],
"edges": [],
"logic_projections": [],
},
"omissions": [],
"diagnostics": {
"selection": "exact_root",
"returned_nodes": 0,
"returned_edges": 0,
"examined_work_units": 0,
"truncated": False,
"ordering": "node_id;source_id,relation,target_id",
},
}
)
@classmethod
def package(
cls,
plan: ManualRenderPlanV1 | GraphViewPlanV1 | None = None,
) -> ProjectionPackageV1:
selected = plan or cls.manual_plan()
kind = "manual" if isinstance(selected, ManualRenderPlanV1) else "graph"
return ProjectionPackageV1.create(
kind=kind,
plan=selected,
renderer={
"renderer_id": "generic_html",
"renderer_version": "1",
},
components=[{"component_id": "projection.document@1"}],
assets=[
{
"asset_id": "projection.template",
"media_type": "text/plain; charset=utf-8",
"sha256": "d" * 64,
"text": "fixture",
}
],
output_policy={
"artifact_ids": ["projection.html"],
"max_total_bytes": 1000000,
},
)
@classmethod
def receipt(cls) -> ProjectionReceiptV1:
package = cls.package()
return ProjectionReceiptV1.create(
kind="manual",
package_id=package.package_id,
plan_id=package.document["plan_id"], # type: ignore[arg-type]
renderer={
"renderer_id": "generic_html",
"renderer_version": "1",
},
artifacts=[
{
"artifact_id": "manual.html",
"media_type": "text/html; charset=utf-8",
"sha256": "e" * 64,
"bytes": 123,
}
],
diagnostics={"warnings": []},
timing={"elapsed_ns": 123456},
peak_memory_bytes=None,
)
def test_schemas_are_valid_and_accept_current_documents(self) -> None:
documents = {
"manual-render-plan.schema.json": self.manual_plan().as_dict(),
"graph-view-plan.schema.json": self.graph_plan().as_dict(),
"projection-package.schema.json": self.package().as_dict(),
"projection-receipt.schema.json": self.receipt().as_dict(),
}
for name, document in documents.items():
with self.subTest(schema=name):
schema = self.schema(name)
Draft202012Validator.check_schema(schema)
Draft202012Validator(schema).validate(document)
graph_package = self.package(self.graph_plan()).as_dict()
self.validator("projection-package.schema.json").validate(graph_package)
def test_unknown_fields_are_rejected_at_contract_boundaries(self) -> None:
cases = (
(
"manual-render-plan.schema.json",
self.manual_plan().as_dict(),
),
(
"graph-view-plan.schema.json",
self.graph_plan().as_dict(),
),
(
"projection-package.schema.json",
self.package().as_dict(),
),
(
"projection-receipt.schema.json",
self.receipt().as_dict(),
),
)
for name, document in cases:
with self.subTest(schema=name):
document["unexpected"] = True
self.assertFalse(self.validator(name).is_valid(document))
manual = self.manual_plan().as_dict()
manual["pages"][0]["unexpected"] = True # type: ignore[index]
self.assertFalse(self.validator("manual-render-plan.schema.json").is_valid(manual))
package = self.package().as_dict()
package["assets"][0]["path"] = "/tmp/escape" # type: ignore[index]
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
receipt = self.receipt().as_dict()
receipt["artifacts"][0]["content"] = "not receipt evidence" # type: ignore[index]
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
def test_obviously_malformed_identities_and_structures_are_rejected(self) -> None:
manual = self.manual_plan().as_dict()
manual["plan_id"] = "not-a-sha256"
self.assertFalse(self.validator("manual-render-plan.schema.json").is_valid(manual))
graph = self.graph_plan().as_dict()
graph["project"]["project_root_fingerprint"] = "wrong" # type: ignore[index]
self.assertFalse(self.validator("graph-view-plan.schema.json").is_valid(graph))
graph = self.graph_plan().as_dict()
graph["bounds"] = -1
self.assertFalse(self.validator("graph-view-plan.schema.json").is_valid(graph))
package = self.package().as_dict()
package["assets"][0].pop("sha256") # type: ignore[index]
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
package = self.package().as_dict()
package["output_policy"]["max_total_bytes"] = 0 # type: ignore[index]
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
package = self.package().as_dict()
package["kind"] = "graph"
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
receipt = self.receipt().as_dict()
receipt["artifacts"][0]["artifact_id"] = "../manual.html" # type: ignore[index]
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
receipt = self.receipt().as_dict()
receipt["artifacts"][0]["bytes"] = -1 # type: ignore[index]
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
receipt = self.receipt().as_dict()
receipt["peak_memory_bytes"] = True
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
def test_assets_and_artifacts_enforce_fixed_collection_bounds(self) -> None:
package = self.package().as_dict()
package["assets"] = [copy.deepcopy(package["assets"][0]) for _ in range(33)] # type: ignore[index]
self.assertFalse(self.validator("projection-package.schema.json").is_valid(package))
receipt = self.receipt().as_dict()
receipt["artifacts"] = [
copy.deepcopy(receipt["artifacts"][0])
for _ in range(33) # type: ignore[index]
]
self.assertFalse(self.validator("projection-receipt.schema.json").is_valid(receipt))
def test_canonical_identity_equality_remains_a_runtime_check(self) -> None:
document = self.manual_plan().as_dict()
document["plan_id"] = "f" * 64
self.validator("manual-render-plan.schema.json").validate(document)
with self.assertRaises(DocForgeError) as raised:
ManualRenderPlanV1.from_dict(document)
self.assertEqual("invalid_projection", raised.exception.code)
if __name__ == "__main__":
unittest.main()

View file

@ -64,6 +64,14 @@ PUBLIC_IMPORTS = {
"docforge.client_config": ("generate_client_configuration",),
"docforge.doctor": ("run_doctor",),
"docforge.index": ("ProjectIndex",),
"docforge.graph_projection": (
"GraphViewRequestV1",
"build_graph_view_plan",
),
"docforge.manual_projection": (
"build_manual_projection_package",
"build_manual_render_plan",
),
"docforge.mcp_server": (
"create_project_server",
"create_read_only_server",
@ -84,6 +92,16 @@ PUBLIC_IMPORTS = {
"capability_mode",
"compose_effective_policy",
),
"docforge.projection_contract": (
"GraphViewPlanV1",
"ManualRenderPlanV1",
"ProjectionArtifact",
"ProjectionPackageV1",
"ProjectionReceiptV1",
"ProjectionRenderResult",
"canonical_projection_bytes",
"projection_hash",
),
"docforge.retrieval": (
"ContextCapsuleV1",
"RetrievalPlanV1",
@ -97,6 +115,7 @@ PUBLIC_IMPORTS = {
"Renderer",
"renderer_for",
),
"docforge_renderers.manual": ("ManualHtmlRenderer",),
}
EXPECTED_ENTRY_POINTS = {

View file

@ -386,6 +386,25 @@ class VisualizationTests(unittest.TestCase):
with self.assertRaisesRegex(DocForgeError, "category is unsupported"):
snapshot.filter_nodes(category="relation", value="depends_on", limit=2)
def test_snapshot_source_never_mixes_pinned_graph_with_newer_canonical_text(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
index = ProjectIndex(Project.open(root))
index.build()
snapshot = VisualizationIndexSnapshot(index, index.check())
before = snapshot.source("guide.workflow")
source = root / "docs/content/workflow.md"
source.write_text(
source.read_text(encoding="utf-8") + "\nNewer unindexed source text.\n",
encoding="utf-8",
)
after = snapshot.source("guide.workflow")
self.assertEqual(before["content"], after["content"])
self.assertNotIn("Newer unindexed source text", after["content"])
self.assertEqual("index_snapshot", after["source_provenance"])
def test_flow_reverses_imports_into_a_complete_structural_path(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))