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

@ -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: