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

Add versioned task context capsules

This commit is contained in:
Andraxion 2026-07-29 07:10:18 -04:00
parent 34cd5f74c1
commit 4cc6277054
18 changed files with 2834 additions and 10 deletions

View file

@ -55,8 +55,10 @@ from docforge.visualization import VisualizationIndexSnapshot
class Loader:
def __init__(self, projection: AdapterProjection) -> None:
self.projection = projection
self.load_calls = 0
def load_projection(self) -> AdapterProjection:
self.load_calls += 1
return self.projection
@ -866,8 +868,9 @@ class AdapterReadOnlyMcpTests(unittest.IsolatedAsyncioTestCase):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory).resolve()
fixture = AdapterContractTests()
loader = Loader(fixture.projection(root))
project = AdapterProject(
Loader(fixture.projection(root)),
loader,
cache_root=root / ".cache" / "adapter-read-only",
)
index = ProjectIndex(project)
@ -903,6 +906,14 @@ class AdapterReadOnlyMcpTests(unittest.IsolatedAsyncioTestCase):
context = await session.call_tool(
"docforge_get_context", {"profile": "fixture", "budget": 321}
)
load_calls = loader.load_calls
task_context = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Do not load the legacy projection for this capability error",
},
)
self.assertEqual(READ_TOOLS, tuple(tool.name for tool in tools.tools))
self.assertEqual("adapter-fixture", info.structuredContent["project_id"])
@ -915,6 +926,12 @@ class AdapterReadOnlyMcpTests(unittest.IsolatedAsyncioTestCase):
self.assertFalse(contract.structuredContent["isolated_changeset_writes_allowed"])
self.assertEqual("fixture", context.structuredContent["profile"])
self.assertEqual([("fixture", 321)], calls)
self.assertEqual("error", task_context.structuredContent["status"])
self.assertEqual(
"task_context_unavailable",
task_context.structuredContent["error"]["code"],
)
self.assertEqual(load_calls, loader.load_calls)
self.assertFalse(project.descriptor.changeset_root.exists())
async def test_adapter_project_proposals_require_explicit_policy_and_stay_isolated(

View file

@ -12,6 +12,7 @@ from contextlib import contextmanager
from pathlib import Path
from unittest import mock
from jsonschema import Draft202012Validator
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from mcp.shared.memory import create_connected_server_and_client_session
@ -34,6 +35,9 @@ from docforge.viewer_manager import ViewerManager
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
CAPSULE_SCHEMA = json.loads(
(ROOT / "schemas" / "context-capsule.schema.json").read_text(encoding="utf-8")
)
class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
@ -85,6 +89,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
self.assertNotIn("limit", tools[name].inputSchema.get("required", []))
for name in (
"docforge_get_context",
"docforge_get_task_context",
"docforge_list_changesets",
"docforge_get_changeset",
"docforge_validate_changeset",
@ -93,6 +98,22 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
for field in ("limit", "cursor"):
self.assertIn(field, tools[name].inputSchema["properties"])
self.assertNotIn(field, tools[name].inputSchema.get("required", []))
self.assertEqual(
{"task_kind", "task"},
set(tools["docforge_get_task_context"].inputSchema["required"]),
)
self.assertEqual(
[
"change",
"implementation",
"failure",
"ownership",
"test",
"operation",
"release",
],
tools["docforge_get_task_context"].inputSchema["properties"]["task_kind"]["enum"],
)
self.assertIn(
"deep",
tools["docforge_render_status"].inputSchema["properties"],
@ -160,6 +181,316 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
self.assertEqual(0, diagnostics["counters"]["project_loads"])
self.assertEqual(0, diagnostics["counters"]["source_files_parsed"])
async def test_task_context_is_hash_stable_paged_and_generation_bound(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
ProjectIndex(Project.open(root)).build()
async with create_connected_server_and_client_session(
create_server(root, capability_mode="read", diagnostics=True),
raise_exceptions=True,
) as session:
first = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
"limit": 1,
},
)
first_capsule = first.structuredContent["capsule"]
Draft202012Validator(CAPSULE_SCHEMA).validate(first_capsule)
cursor = first_capsule["pagination"]["next_cursor"]
hashes = {
first_capsule["capsule_hash"],
first_capsule["collection_hash"],
first_capsule["plan"]["plan_hash"],
}
evidence_ids = [item["node_id"] for item in first_capsule["evidence"]]
while cursor is not None:
page = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
"limit": 2,
"cursor": cursor,
},
)
capsule = page.structuredContent["capsule"]
self.assertEqual(first_capsule["capsule_hash"], capsule["capsule_hash"])
self.assertEqual(
first_capsule["collection_hash"],
capsule["collection_hash"],
)
self.assertEqual(
first_capsule["plan"]["plan_hash"],
capsule["plan"]["plan_hash"],
)
evidence_ids.extend(item["node_id"] for item in capsule["evidence"])
cursor = capsule["pagination"]["next_cursor"]
self.assertEqual(
["guide.workflow", "guide.foundation", "proof.validation"],
evidence_ids,
)
self.assertEqual(3, len(hashes))
self.assertEqual(
"mcp.task_context",
first.structuredContent["diagnostics"]["operation"],
)
counters = first.structuredContent["diagnostics"]["counters"]
self.assertEqual(0, counters["project_loads"])
self.assertEqual(0, counters["source_files_parsed"])
self.assertEqual(0, counters["adapter_projection_loads"])
self.assertEqual(0, counters["adapter_source_extractions"])
self.assertEqual(0, counters["index_builds"])
self.assertLessEqual(
len(json.dumps(first.structuredContent, separators=(",", ":"))),
Project.open(root).descriptor.limits.max_tool_output_chars,
)
for changed_arguments in (
{"task": "A different task"},
{"task_kind": "failure"},
{"focus_node_id": "guide.foundation"},
{"budget": 100},
):
arguments = {
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
"limit": 1,
"cursor": first_capsule["pagination"]["next_cursor"],
**changed_arguments,
}
changed_cursor = await session.call_tool(
"docforge_get_task_context",
arguments,
)
self.assertEqual(
"stale_cursor",
changed_cursor.structuredContent["error"]["code"],
)
different_policy = DocForgeService(
Project.open(root),
capability_mode_name="proposal",
).task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
limit=1,
cursor=first_capsule["pagination"]["next_cursor"],
)
self.assertEqual("stale_cursor", different_policy["error"]["code"])
changed = root / "docs/content/foundation.md"
changed.write_text(
changed.read_text(encoding="utf-8") + "\nNew generation.\n",
encoding="utf-8",
)
stale = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
"limit": 1,
"cursor": first_capsule["pagination"]["next_cursor"],
},
)
self.assertEqual("error", stale.structuredContent["status"])
self.assertEqual("stale_cursor", stale.structuredContent["error"]["code"])
async def test_custom_context_policy_does_not_silently_gain_task_planning(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
project = Project.open(root)
ProjectIndex(project).build()
service = DocForgeService(
project,
context_provider=lambda index, profile, budget: {
"profile": profile,
"budget": budget,
"entries": [],
"omissions": [],
},
capability_mode_name="read",
)
async with create_connected_server_and_client_session(
_create_bound_server(service, read_only=True),
raise_exceptions=True,
) as session:
bootstrap = await session.call_tool("docforge_bootstrap", {})
with (
mock.patch.object(
service.project,
"load",
side_effect=AssertionError("capability errors must not load"),
),
mock.patch.object(
service.index,
"check",
side_effect=AssertionError("capability errors must not check"),
),
mock.patch.object(
service.index,
"build",
side_effect=AssertionError("capability errors must not build"),
),
mock.patch.object(
service.index,
"synchronize",
side_effect=AssertionError("capability errors must not synchronize"),
),
):
result = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Do not widen the adapter context policy",
},
)
self.assertEqual(
"docforge_get_context",
bootstrap.structuredContent["session_contract"]["recommended_first_operation"][
"tool"
],
)
self.assertFalse(bootstrap.structuredContent["capabilities"]["task_context"]["enabled"])
self.assertEqual("error", result.structuredContent["status"])
self.assertEqual(
"task_context_unavailable",
result.structuredContent["error"]["code"],
)
def test_task_context_page_hash_binds_final_page_envelope(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
"max_context_tokens = 2000",
"max_context_tokens = 2000\nmax_tool_output_chars = 8000",
),
encoding="utf-8",
)
project = Project.open(root)
ProjectIndex(project).build()
service = DocForgeService(project, capability_mode_name="read")
one = service.task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
limit=1,
)
two = service.task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
limit=2,
)
one_capsule = one["capsule"]
two_capsule = two["capsule"]
self.assertEqual(
["guide.workflow"],
[item["node_id"] for item in one_capsule["evidence"]],
)
self.assertEqual(
["guide.workflow"],
[item["node_id"] for item in two_capsule["evidence"]],
)
self.assertEqual("complete", one_capsule["page_state"])
self.assertEqual("incomplete", two_capsule["page_state"])
self.assertNotEqual(one_capsule["page_hash"], two_capsule["page_hash"])
self.assertNotEqual(one_capsule["pagination"], two_capsule["pagination"])
self.assertLessEqual(
len(json.dumps(one, sort_keys=True, separators=(",", ":"))),
8_000,
)
self.assertLessEqual(
len(json.dumps(two, sort_keys=True, separators=(",", ":"))),
8_000,
)
def test_task_context_default_page_clamps_to_small_project_limit(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
"max_results = 20",
"max_results = 2",
),
encoding="utf-8",
)
project = Project.open(root)
ProjectIndex(project).build()
result = DocForgeService(
project,
capability_mode_name="read",
).task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
)
self.assertEqual(2, result["pagination"]["limit"])
self.assertLessEqual(result["pagination"]["returned_count"], 2)
def test_oversized_task_evidence_advances_once_as_an_omission(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
"max_context_tokens = 2000",
"max_context_tokens = 50000\nmax_tool_output_chars = 8000",
),
encoding="utf-8",
)
workflow = root / "docs" / "content" / "workflow.md"
workflow.write_text(
workflow.read_text(encoding="utf-8") + "\n" + ("large evidence " * 2_000),
encoding="utf-8",
)
project = Project.open(root)
ProjectIndex(project).build()
service = DocForgeService(project, capability_mode_name="read")
first = service.task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
budget=50_000,
limit=1,
)
first_capsule = first["capsule"]
self.assertEqual([], first_capsule["evidence"])
self.assertEqual("response_limit", first_capsule["omissions"][0]["code"])
self.assertEqual("guide.workflow", first_capsule["omissions"][0]["subject"])
self.assertEqual(1, first_capsule["pagination"]["returned_count"])
self.assertTrue(first_capsule["pagination"]["has_more"])
second = service.task_context(
"change",
"Change the editing workflow",
focus_node_id="guide.workflow",
budget=50_000,
limit=1,
cursor=first_capsule["pagination"]["next_cursor"],
)
self.assertNotEqual(first_capsule["page_hash"], second["capsule"]["page_hash"])
self.assertNotIn(
"guide.workflow",
[
item.get("node_id", item.get("subject"))
for item in (
*second["capsule"]["evidence"],
*second["capsule"]["omissions"],
)
],
)
async def test_context_pagination_is_complete_and_stale_cursors_fail_closed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
@ -304,6 +635,14 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
logic = await session.call_tool(
"docforge_get_logic", {"owner_node_id": "guide.workflow"}
)
task_context = await session.call_tool(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Change the editing workflow without AST analysis",
"focus_node_id": "guide.workflow",
},
)
policy = bootstrap.structuredContent["adapter_policy"]
self.assertEqual("preserve-no-ast", policy["mode"])
@ -329,6 +668,14 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
"adapter_policy_forbids_logic",
logic.structuredContent["error"]["code"],
)
self.assertEqual("ok", task_context.structuredContent["status"])
self.assertNotIn(
"logic",
{
step["operation"]
for step in task_context.structuredContent["capsule"]["plan"]["steps"]
},
)
async def test_every_read_tool_returns_scoped_structured_results(self) -> None:
with tempfile.TemporaryDirectory() as directory:
@ -358,6 +705,14 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
("docforge_visualization_status", {}),
("docforge_bootstrap", {}),
("docforge_sync", {}),
(
"docforge_get_task_context",
{
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
},
),
)
with self.running_manager(Path(directory) / "viewer-manager.json"):
service = DocForgeService(Project.open(root))
@ -413,6 +768,7 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
context = results[9].structuredContent
self.assertLessEqual(context["estimated_tokens"], 180)
self.assertTrue(context["omissions"])
self.assertEqual("complete", results[17].structuredContent["capsule"]["state"])
async def test_invalid_traversal_limit_is_a_structured_domain_error(self) -> None:
with tempfile.TemporaryDirectory() as directory:

View file

@ -147,7 +147,7 @@ class EffectivePolicyTests(unittest.TestCase):
self.assertNotIn("docforge_register_changes", result["recommended_workflow"])
self.assertNotIn("docforge_apply_changeset", result["recommended_workflow"])
self.assertEqual(
"docforge_get_context",
"docforge_get_task_context",
result["session_contract"]["recommended_first_operation"]["tool"],
)

View file

@ -82,6 +82,13 @@ PUBLIC_IMPORTS = {
"capability_mode",
"compose_effective_policy",
),
"docforge.retrieval": (
"ContextCapsuleV1",
"RetrievalPlanV1",
"build_retrieval_plan",
"relation_category",
"validate_retrieval_plan",
),
"docforge.render_contract": (
"GenericHtmlRenderer",
"PreparedRender",
@ -132,6 +139,7 @@ EXPECTED_MCP_TOOLS = {
"docforge_get_changeset",
"docforge_get_changeset_diff",
"docforge_get_context",
"docforge_get_task_context",
"docforge_get_contract",
"docforge_get_logic",
"docforge_get_node",

464
tests/test_retrieval.py Normal file
View file

@ -0,0 +1,464 @@
from __future__ import annotations
import json
import shutil
import tempfile
import unittest
from dataclasses import replace
from pathlib import Path
from unittest import mock
from jsonschema import Draft202012Validator
from docforge.errors import DocForgeError
from docforge.index import ProjectIndex
from docforge.models import ProjectState
from docforge.policy import compose_effective_policy
from docforge.project import Project
from docforge.retrieval import (
BASE_RELATION_CATEGORIES,
MAX_TASK_CANDIDATE_EDGES,
MAX_TASK_EVIDENCE,
RELATION_CATEGORIES,
TASK_KINDS,
TASK_REQUIREMENTS,
build_retrieval_plan,
relation_category,
)
ROOT = Path(__file__).resolve().parents[1]
FIXTURES = ROOT / "tests" / "fixtures"
CAPSULE_SCHEMA = json.loads(
(ROOT / "schemas" / "context-capsule.schema.json").read_text(encoding="utf-8")
)
class TaskRetrievalTests(unittest.TestCase):
def copy_fixture(self, name: str, destination: Path) -> Path:
root = destination / name
shutil.copytree(FIXTURES / name, root)
return root
@staticmethod
def effective_policy() -> dict[str, object]:
return compose_effective_policy(
selected_mode="read",
capability_source="explicit",
no_ast=False,
diagnostics=False,
render_configured=True,
application_enabled=False,
).as_dict()
def plan(
self,
project: Project,
*,
task_kind: str = "change",
task: str = "Change the editing workflow",
focus_node_id: str | None = "guide.workflow",
budget: int | None = None,
limit: int | None = None,
):
return build_retrieval_plan(
project.descriptor,
task_kind=task_kind,
task=task,
focus_node_id=focus_node_id,
budget=budget,
limit=limit,
effective_policy=self.effective_policy(),
)
def test_every_task_plan_is_closed_deterministic_and_hash_bound(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
hashes = {}
relation_hashes = set()
for task_kind in TASK_KINDS:
first = self.plan(project, task_kind=task_kind)
second = self.plan(project, task_kind=task_kind)
self.assertEqual(first, second)
self.assertEqual(TASK_REQUIREMENTS[task_kind], first.category_order[:1])
self.assertEqual(
{
*BASE_RELATION_CATEGORIES,
"unclassified",
},
set(first.category_order),
)
self.assertEqual(64, len(first.plan_hash))
relation_hashes.update(
step.relation_set_hash
for step in first.steps
if step.relation_scope == "project_allowed"
)
hashes[task_kind] = first.plan_hash
self.assertEqual(len(TASK_KINDS), len(set(hashes.values())))
self.assertEqual(1, len(relation_hashes))
self.assertEqual(
{
"change": ("dependency",),
"implementation": ("implementation",),
"failure": ("execution",),
"ownership": ("structure",),
"test": ("evidence",),
"operation": ("execution",),
"release": ("evidence",),
},
TASK_REQUIREMENTS,
)
mapped = [
relation for relations in RELATION_CATEGORIES.values() for relation in relations
]
self.assertEqual(len(mapped), len(set(mapped)))
self.assertEqual(
{
"structure": ("contains", "defined_in", "defines", "owns"),
"implementation": (
"implemented_by",
"implements",
"inherits",
"inherits_from",
),
"dependency": ("depends_on", "imports"),
"execution": (
"activates",
"calls",
"dispatches_to",
"launches",
),
"data": ("reads", "writes"),
"evidence": (
"documents",
"governs",
"proves",
"tested_by",
"verifies",
),
"context": ("relates_to",),
"unclassified": (),
},
RELATION_CATEGORIES,
)
self.assertEqual("dependency", relation_category("depends_on"))
self.assertEqual("evidence", relation_category("proves"))
self.assertEqual("unclassified", relation_category("owns_database"))
self.assertEqual("unclassified", relation_category("when_true"))
def test_exact_capsule_is_schema_valid_stable_and_explainable(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
index = ProjectIndex(project)
index.build()
plan = self.plan(project)
first = index.task_context(plan)
second = index.task_context(plan)
self.assertEqual(first, second)
capsule = first["capsule"]
Draft202012Validator(CAPSULE_SCHEMA).validate(capsule)
self.assertEqual("complete", capsule["state"])
self.assertEqual("resolved", capsule["focus"]["state"])
self.assertEqual("guide.workflow", capsule["focus"]["node_id"])
self.assertEqual(
["guide.workflow", "guide.foundation", "proof.validation"],
[item["node_id"] for item in capsule["evidence"]],
)
dependency = capsule["evidence"][1]["relationship_path"][0]
self.assertEqual("depends_on", dependency["relation"])
self.assertEqual("dependency", dependency["category"])
self.assertEqual("outgoing", dependency["direction"])
workflow_reasons = capsule["evidence"][0]["relationship_reasons"]
self.assertIn(
("depends_on", "outgoing"),
{
(relationship["relation"], relationship["direction"])
for relationship in workflow_reasons
},
)
self.assertIn(
("proves", "incoming"),
{
(relationship["relation"], relationship["direction"])
for relationship in workflow_reasons
},
)
foundation_reason = capsule["evidence"][1]["relationship_reasons"][0]
self.assertEqual("depends_on", foundation_reason["relation"])
self.assertEqual("incoming", foundation_reason["direction"])
self.assertNotIn(
"no_selected_evidence",
[gap["code"] for gap in capsule["gaps"]],
)
self.assertEqual(64, len(capsule["collection_hash"]))
self.assertEqual(64, len(capsule["capsule_hash"]))
def test_gaps_distinguish_undeclared_complete_and_incomplete_checks(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
project = Project.open(root)
index = ProjectIndex(project)
index.build()
undeclared = index.task_context(self.plan(project, task_kind="implementation"))[
"capsule"
]
self.assertIn(
"category_not_declared",
[gap["code"] for gap in undeclared["gaps"]],
)
descriptor = root / ".docforge" / "project.toml"
descriptor.write_text(
descriptor.read_text(encoding="utf-8").replace(
'"returns_to"]',
'"returns_to", "calls"]',
),
encoding="utf-8",
)
declared_project = Project.open(root)
declared_index = ProjectIndex(declared_project)
declared_index.build()
missing = declared_index.task_context(
self.plan(
declared_project,
task_kind="failure",
focus_node_id="guide.foundation",
)
)["capsule"]
self.assertIn(
"no_selected_evidence",
[gap["code"] for gap in missing["gaps"]],
)
incomplete = declared_index.task_context(self.plan(declared_project, budget=1))[
"capsule"
]
self.assertEqual("incomplete", incomplete["state"])
self.assertTrue(incomplete["omissions"])
self.assertIn(
"evidence_incomplete",
[gap["code"] for gap in incomplete["gaps"]],
)
self.assertNotIn(
"no_selected_evidence",
[gap["code"] for gap in incomplete["gaps"]],
)
def test_unclassified_relation_is_preserved_without_guessed_semantics(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("awesome-ski-game", Path(directory)))
index = ProjectIndex(project)
index.build()
plan = build_retrieval_plan(
project.descriptor,
task_kind="change",
task="Change the first descent session",
focus_node_id="session.first-descent",
budget=None,
limit=None,
effective_policy=self.effective_policy(),
)
capsule = index.task_context(plan)["capsule"]
self.assertIn("informs", capsule["summary"]["unclassified_relations"])
relation = next(
relationship
for item in capsule["evidence"]
for relationship in item["relationship_reasons"]
if relationship["relation"] == "informs"
)
self.assertEqual("unclassified", relation["category"])
self.assertIn(
"unclassified_relation",
[gap["code"] for gap in capsule["gaps"]],
)
def test_plan_is_compact_at_large_valid_relation_scale_and_limits_are_fixed(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
relation_names = tuple(f"relation-{position:05d}" for position in range(33_005))
descriptor = replace(
project.descriptor,
allowed_relations=relation_names,
limits=replace(
project.descriptor.limits,
max_results=2**63,
),
)
plan = build_retrieval_plan(
descriptor,
task_kind="change",
task="Exercise a very large valid relation policy",
focus_node_id="guide.workflow",
budget=None,
limit=None,
effective_policy=self.effective_policy(),
)
self.assertLessEqual(plan.max_evidence, MAX_TASK_EVIDENCE)
self.assertLessEqual(plan.max_candidate_edges, MAX_TASK_CANDIDATE_EDGES)
traversal = [step for step in plan.steps if step.operation in {"outgoing", "incoming"}]
self.assertEqual(2, len(traversal))
self.assertTrue(all(step.relation_scope == "project_allowed" for step in traversal))
self.assertTrue(all(len(step.relation_set_hash or "") == 64 for step in traversal))
self.assertLess(len(json.dumps(plan.as_dict())), 5_000)
def test_executor_rejects_tampered_public_plan_objects(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
index = ProjectIndex(project)
index.build()
plan = self.plan(project)
tampered = (
replace(plan, steps=plan.steps[:-1]),
replace(plan, requirements=()),
replace(plan, task_query="Different task"),
replace(plan, category_order=tuple(reversed(plan.category_order))),
replace(plan, max_evidence=plan.max_evidence + 1),
replace(plan, effective_policy_hash="0" * 64),
replace(plan, plan_hash="0" * 64),
)
for candidate in tampered:
with self.subTest(candidate=candidate):
with self.assertRaises(DocForgeError) as invalid:
index.task_context(candidate)
self.assertEqual("invalid_retrieval_plan", invalid.exception.code)
def test_work_and_unclassified_limits_are_explicit_and_bounded(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
relation_names = [f"relation-{position:03d}" for position in range(450)]
descriptor = root / ".docforge" / "project.toml"
raw_descriptor = descriptor.read_text(encoding="utf-8")
raw_relations = ", ".join(
json.dumps(relation)
for relation in (
"depends_on",
"proves",
"supersedes",
"relates_to",
"returns_to",
*relation_names,
)
)
descriptor.write_text(
raw_descriptor.replace(
'"depends_on", "proves", "supersedes", "relates_to", "returns_to"',
raw_relations,
),
encoding="utf-8",
)
workflow = root / "docs" / "content" / "workflow.md"
raw_workflow = workflow.read_text(encoding="utf-8")
relationships = "".join(
f'{relation} = ["guide.foundation"]\n' for relation in relation_names
)
workflow.write_text(
raw_workflow.replace("+++\n\nEditors", f"{relationships}+++\n\nEditors", 1),
encoding="utf-8",
)
project = Project.open(root)
index = ProjectIndex(project)
index.build()
capsule = index.task_context(self.plan(project))["capsule"]
Draft202012Validator(CAPSULE_SCHEMA).validate(capsule)
omission_codes = {omission["code"] for omission in capsule["omissions"]}
self.assertIn("edge_examination_limit", omission_codes)
self.assertIn("unclassified_relation_limit", omission_codes)
self.assertEqual(
project.descriptor.limits.max_results,
len(capsule["summary"]["unclassified_relations"]),
)
self.assertLessEqual(
capsule["summary"]["examined_edge_count"],
MAX_TASK_CANDIDATE_EDGES,
)
def test_lexical_focus_blocks_missing_and_ambiguous_selection(self) -> None:
with tempfile.TemporaryDirectory() as directory:
root = self.copy_fixture("alpha", Path(directory))
project = Project.open(root)
index = ProjectIndex(project)
index.build()
missing = index.task_context(
self.plan(
project,
task="Words absent from every indexed node",
focus_node_id=None,
)
)["capsule"]
self.assertEqual("blocked", missing["state"])
self.assertEqual("not_found", missing["focus"]["state"])
self.assertEqual("focus_not_found", missing["gaps"][0]["code"])
duplicate = root / "docs" / "content" / "workflow-copy.md"
duplicate.write_text(
(root / "docs" / "content" / "workflow.md")
.read_text(encoding="utf-8")
.replace('id = "guide.workflow"', 'id = "guide.workflow-copy"'),
encoding="utf-8",
)
duplicate_project = Project.open(root)
duplicate_index = ProjectIndex(duplicate_project)
duplicate_index.build()
ambiguous = duplicate_index.task_context(
self.plan(
duplicate_project,
task="Editing workflow",
focus_node_id=None,
)
)["capsule"]
self.assertEqual("blocked", ambiguous["state"])
self.assertEqual("ambiguous", ambiguous["focus"]["state"])
self.assertEqual("focus_ambiguous", ambiguous["gaps"][0]["code"])
def test_plan_rejects_unbounded_or_untyped_inputs(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
cases = (
({"task_kind": "unknown"}, "invalid_task_kind"),
({"task": " "}, "invalid_task_focus"),
({"focus_node_id": ""}, "invalid_task_focus"),
({"budget": True}, "invalid_budget"),
({"budget": 0}, "invalid_budget"),
({"limit": True}, "invalid_limit"),
({"limit": project.descriptor.limits.max_results + 1}, "invalid_limit"),
)
for overrides, code in cases:
arguments = {
"task_kind": "change",
"task": "Change the editing workflow",
"focus_node_id": "guide.workflow",
"budget": None,
"limit": None,
**overrides,
}
with self.subTest(arguments=overrides):
with self.assertRaises(DocForgeError) as invalid:
self.plan(project, **arguments)
self.assertEqual(code, invalid.exception.code)
def test_final_generation_change_rejects_the_whole_capsule(self) -> None:
with tempfile.TemporaryDirectory() as directory:
project = Project.open(self.copy_fixture("alpha", Path(directory)))
index = ProjectIndex(project)
built = index.build()
current = ProjectState(
revision=built["revision"],
source_hash=built["source_hash"],
)
changed = ProjectState(
revision=current.revision,
source_hash="0" * 64,
)
with (
mock.patch.object(
project,
"incremental_state",
side_effect=(current, changed),
),
self.assertRaises(DocForgeError) as stale,
):
index.task_context(self.plan(project))
self.assertEqual("source_changed", stale.exception.code)