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

@ -14,7 +14,7 @@ from collections.abc import Callable, Generator
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import cast
from typing import Literal, cast
from .errors import DocForgeError
from .models import (
@ -32,11 +32,25 @@ from .models import (
ProjectSnapshot,
ProjectState,
)
from .pagination import canonical_hash
from .project import project_root_fingerprint
from .retrieval import (
CapsuleEvidenceV1,
CapsuleOmissionV1,
CapsuleRelationshipV1,
ContextCapsuleV1,
EvidenceGapV1,
RetrievalPlanV1,
capsule_evidence,
finalize_capsule,
relation_category,
validate_retrieval_plan,
)
from .telemetry import increment, stage
INDEX_SCHEMA_VERSION = 3
APPLICATION_ID = 1_146_683_778
_SQLITE_PARAMETER_CHUNK = 500
def _node_hash(nodes: tuple[Node, ...]) -> str:
@ -147,6 +161,17 @@ class _IndexReadSnapshot:
}
@dataclass(frozen=True)
class _TaskSelection:
node_id: str
role: str
reason_code: str
depth: int
category_rank: int
first_edge: tuple[str, str, str]
relationship_path: tuple[CapsuleRelationshipV1, ...]
class ProjectIndex:
"""A disposable index that always checks current canonical source before queries."""
@ -798,6 +823,418 @@ class ProjectIndex:
status.st_ctime_ns,
)
def task_context(self, plan: RetrievalPlanV1) -> dict[str, object]:
"""Execute one fixed task plan inside one immutable index generation."""
validate_retrieval_plan(plan, self.project.descriptor)
with self._read_snapshot() as snapshot:
capsule = self._task_context_capsule(snapshot, plan)
return snapshot.result(capsule=capsule.as_dict())
def _task_context_capsule(
self,
snapshot: _IndexReadSnapshot,
plan: RetrievalPlanV1,
) -> ContextCapsuleV1:
generation = {
"project_id": snapshot.checked["project_id"],
"project_root_fingerprint": snapshot.checked["project_root_fingerprint"],
"adapter": snapshot.checked["adapter"],
"revision": snapshot.checked["revision"],
"source_hash": snapshot.checked["source_hash"],
"index_schema_version": INDEX_SCHEMA_VERSION,
}
focus_rows: list[sqlite3.Row]
focus_reason = "exact_focus"
if plan.focus_node_id is not None:
row = snapshot.connection.execute(
"SELECT * FROM nodes WHERE node_id = ?",
(plan.focus_node_id,),
).fetchone()
if row is None:
raise DocForgeError(
"missing_node",
"No node has the requested stable ID",
node_id=plan.focus_node_id,
)
focus_rows = [row]
else:
terms = re_tokenize(plan.task_query)
if not terms:
raise DocForgeError(
"invalid_task_focus",
"Task description contains no searchable text",
)
expression = " AND ".join(f'"{term.replace(chr(34), chr(34) * 2)}"' for term in terms)
focus_rows = snapshot.connection.execute(
"""
SELECT nodes.*, bm25(node_fts) AS rank
FROM node_fts JOIN nodes USING(node_id)
WHERE node_fts MATCH ?
ORDER BY rank, nodes.node_id
LIMIT ?
""",
(expression, plan.max_evidence + 1),
).fetchall()
focus_reason = "lexical_focus"
if not focus_rows:
return finalize_capsule(
state="blocked",
task_kind=plan.task_kind,
generation=generation,
plan=plan,
focus_state="not_found",
focus_node_id=None,
focus_candidate_count=0,
evidence=(),
gaps=(
EvidenceGapV1(
code="focus_not_found",
requirement_id="focus",
category=None,
state="blocked",
check_complete=True,
detail="No indexed node matched the bounded lexical focus.",
),
),
omissions=(),
selected_count=0,
examined_edge_count=0,
estimated_tokens=0,
unclassified_relations=(),
)
if len(focus_rows) > 1 and focus_rows[0]["rank"] == focus_rows[1]["rank"]:
return finalize_capsule(
state="blocked",
task_kind=plan.task_kind,
generation=generation,
plan=plan,
focus_state="ambiguous",
focus_node_id=None,
focus_candidate_count=len(focus_rows),
evidence=(),
gaps=(
EvidenceGapV1(
code="focus_ambiguous",
requirement_id="focus",
category=None,
state="blocked",
check_complete=True,
detail=(
"The highest-ranked lexical focus is tied; provide focus_node_id."
),
),
),
omissions=(),
selected_count=0,
examined_edge_count=0,
estimated_tokens=0,
unclassified_relations=(),
)
focus_rows = focus_rows[:1]
focus_node_id = cast(str, focus_rows[0]["node_id"])
selections: dict[str, _TaskSelection] = {
focus_node_id: _TaskSelection(
node_id=focus_node_id,
role="focus",
reason_code=focus_reason,
depth=0,
category_rank=-1,
first_edge=("", "", ""),
relationship_path=(),
)
}
relationship_reasons: dict[str, set[CapsuleRelationshipV1]] = {focus_node_id: set()}
queue: deque[str] = deque((focus_node_id,))
examined_edge_count = 0
traversal_incomplete = False
edge_examination_limit_reached = False
first_omitted_node: str | None = None
unclassified_relations: set[str] = set()
while queue and not traversal_incomplete:
current = queue.popleft()
current_selection = selections[current]
if current_selection.depth >= plan.max_depth:
continue
remaining = plan.max_candidate_edges - examined_edge_count
if remaining <= 0:
traversal_incomplete = True
edge_examination_limit_reached = True
break
outgoing = snapshot.connection.execute(
"SELECT source_id, relation, target_id FROM edges "
"WHERE source_id = ? ORDER BY source_id, relation, target_id LIMIT ?",
(current, remaining + 1),
).fetchall()
incoming = snapshot.connection.execute(
"SELECT source_id, relation, target_id FROM edges "
"WHERE target_id = ? ORDER BY source_id, relation, target_id LIMIT ?",
(current, remaining + 1),
).fetchall()
candidates = {
(
cast(str, row["source_id"]),
cast(str, row["relation"]),
cast(str, row["target_id"]),
"outgoing" if row["source_id"] == current else "incoming",
)
for row in (*outgoing, *incoming)
}
ordered = sorted(
candidates,
key=lambda item: (
plan.category_order.index(relation_category(item[1])),
item[0],
item[1],
item[2],
item[3],
),
)
if len(ordered) > remaining:
traversal_incomplete = True
edge_examination_limit_reached = True
ordered = ordered[:remaining]
for source_id, relation, target_id, direction in ordered:
examined_edge_count += 1
category = relation_category(relation)
if category == "unclassified":
unclassified_relations.add(relation)
neighbor = target_id if source_id == current else source_id
relationship = CapsuleRelationshipV1(
source_id=source_id,
relation=relation,
target_id=target_id,
direction=cast(Literal["outgoing", "incoming"], direction),
category=category,
)
evidence_reason = CapsuleRelationshipV1(
source_id=source_id,
relation=relation,
target_id=target_id,
direction="outgoing" if source_id == neighbor else "incoming",
category=category,
)
if neighbor in selections:
relationship_reasons.setdefault(neighbor, set()).add(evidence_reason)
continue
if len(selections) >= plan.max_evidence:
traversal_incomplete = True
first_omitted_node = neighbor
break
selections[neighbor] = _TaskSelection(
node_id=neighbor,
role="related",
reason_code="relationship_path",
depth=current_selection.depth + 1,
category_rank=plan.category_order.index(category),
first_edge=(source_id, relation, target_id),
relationship_path=(
*current_selection.relationship_path,
relationship,
),
)
relationship_reasons[neighbor] = {evidence_reason}
queue.append(neighbor)
ordered_selections = sorted(
selections.values(),
key=lambda selection: (
0 if selection.role == "focus" else 1,
selection.category_rank,
selection.depth,
selection.first_edge,
selection.node_id,
),
)
selected_node_ids = tuple(selection.node_id for selection in ordered_selections)
rows: list[sqlite3.Row] = []
for position in range(0, len(selected_node_ids), _SQLITE_PARAMETER_CHUNK):
node_id_chunk = selected_node_ids[position : position + _SQLITE_PARAMETER_CHUNK]
placeholders = ",".join("?" for _ in node_id_chunk)
rows.extend(
snapshot.connection.execute(
f"SELECT * FROM nodes WHERE node_id IN ({placeholders}) ORDER BY node_id",
node_id_chunk,
).fetchall()
)
nodes = {cast(str, row["node_id"]): _row_to_node(row) for row in rows}
evidence: list[CapsuleEvidenceV1] = []
omissions: list[CapsuleOmissionV1] = []
used_tokens = 0
returned_categories: set[str] = set()
for selection in ordered_selections:
node = nodes[selection.node_id]
item = capsule_evidence(
role=cast(Literal["focus", "related"], selection.role),
reason_code=cast(
Literal["exact_focus", "lexical_focus", "relationship_path"],
selection.reason_code,
),
node=node,
depth=selection.depth,
relationship_path=selection.relationship_path,
relationship_reasons=tuple(
sorted(
relationship_reasons.get(selection.node_id, set()),
key=lambda relationship: (
relationship.source_id,
relationship.relation,
relationship.target_id,
relationship.direction,
),
)
),
)
if used_tokens + item.estimated_tokens > plan.max_tokens:
omissions.append(
CapsuleOmissionV1(
code="token_budget",
subject=node.node_id,
detail_hash=canonical_hash(
{
"node_id": node.node_id,
"content_hash": node.content_hash,
"estimated_tokens": item.estimated_tokens,
}
),
)
)
continue
evidence.append(item)
used_tokens += item.estimated_tokens
returned_categories.update(
relationship.category for relationship in item.relationship_reasons
)
if first_omitted_node is not None:
omissions.append(
CapsuleOmissionV1(
code="result_limit",
subject=first_omitted_node,
detail_hash=canonical_hash(
{
"node_id": first_omitted_node,
"max_evidence": plan.max_evidence,
}
),
)
)
if edge_examination_limit_reached:
omissions.append(
CapsuleOmissionV1(
code="edge_examination_limit",
subject=focus_node_id,
detail_hash=canonical_hash(
{
"focus_node_id": focus_node_id,
"examined_edge_count": examined_edge_count,
"max_candidate_edges": plan.max_candidate_edges,
}
),
)
)
ordered_unclassified_relations = tuple(sorted(unclassified_relations))
bounded_unclassified_relations = ordered_unclassified_relations[: plan.max_evidence]
if len(ordered_unclassified_relations) > len(bounded_unclassified_relations):
omissions.append(
CapsuleOmissionV1(
code="unclassified_relation_limit",
subject=focus_node_id,
detail_hash=canonical_hash(
{
"unclassified_relation_count": len(ordered_unclassified_relations),
"returned_count": len(bounded_unclassified_relations),
}
),
)
)
declared_categories = {
relation_category(relation) for relation in self.project.descriptor.allowed_relations
}
gaps: list[EvidenceGapV1] = []
incomplete = traversal_incomplete or bool(omissions)
for requirement in plan.requirements:
category = requirement.category
if category not in declared_categories:
gaps.append(
EvidenceGapV1(
code="category_not_declared",
requirement_id=requirement.requirement_id,
category=category,
state="missing",
check_complete=True,
detail=(
"No declared project relation maps to this required plan category."
),
)
)
elif category in returned_categories:
continue
elif incomplete:
gaps.append(
EvidenceGapV1(
code="evidence_incomplete",
requirement_id=requirement.requirement_id,
category=category,
state="incomplete",
check_complete=False,
detail=(
"A bounded work, result, or token limit prevented a complete "
"returned-evidence proof."
),
)
)
else:
gaps.append(
EvidenceGapV1(
code="no_selected_evidence",
requirement_id=requirement.requirement_id,
category=category,
state="missing",
check_complete=True,
detail=(
"The complete bounded check found no selected graph evidence in "
"this category."
),
)
)
for relation in bounded_unclassified_relations:
gaps.append(
EvidenceGapV1(
code="unclassified_relation",
requirement_id=f"relation.{relation}",
category="unclassified",
state="limitation",
check_complete=True,
detail="The project relation is preserved without inferred task semantics.",
)
)
gaps.sort(
key=lambda gap: (
gap.requirement_id,
gap.code,
"" if gap.category is None else gap.category,
)
)
return finalize_capsule(
state="incomplete" if incomplete else "complete",
task_kind=plan.task_kind,
generation=generation,
plan=plan,
focus_state="resolved",
focus_node_id=focus_node_id,
focus_candidate_count=len(focus_rows),
evidence=tuple(evidence),
gaps=tuple(gaps),
omissions=tuple(omissions),
selected_count=len(selections),
examined_edge_count=examined_edge_count,
estimated_tokens=used_tokens,
unclassified_relations=bounded_unclassified_relations,
)
def get_node(self, node_id: str) -> dict[str, object]:
with self._read_snapshot() as snapshot:
row = snapshot.connection.execute(

View file

@ -21,6 +21,7 @@ from .pagination import canonical_hash, decode_cursor, page_limit, page_receipt
from .policy import CapabilityMode, capability_mode, compose_effective_policy
from .project import Project, project_root_fingerprint
from .rendering import RenderService
from .retrieval import MAX_TASK_EVIDENCE, TaskKind, build_retrieval_plan
from .telemetry import request, stage
from .viewer_manager import ViewerManagerClient
@ -43,6 +44,7 @@ READ_TOOLS = (
"docforge_dependencies",
"docforge_impact",
"docforge_get_context",
"docforge_get_task_context",
"docforge_validate_project",
"docforge_render_status",
"docforge_visualize",
@ -164,6 +166,7 @@ class DocForgeService:
)
self.visualization = ViewerManagerClient(self.index)
self.context_provider = context_provider
self.task_context_available = context_provider is compile_context
self.binding_metadata = dict(binding_metadata or {})
self.no_ast = self.policy.no_ast
self.diagnostics = diagnostics
@ -199,6 +202,14 @@ class DocForgeService:
"enabled": True,
"tools": [tool for tool in READ_TOOLS if tool in self.tool_surface],
},
"task_context": {
"enabled": self.task_context_available,
"reason": (
None
if self.task_context_available
else "custom_context_policy_not_supported_by_task_context_v1"
),
},
"proposal": {
"surface_enabled": any(tool in self.tool_surface for tool in PROPOSAL_TOOLS),
"mutation_access": proposal_access,
@ -558,7 +569,11 @@ class DocForgeService:
"adapter_policy": self.adapter_policy(),
}
recommended_workflow = [
"docforge_get_context or targeted read tools",
(
"docforge_get_task_context, docforge_get_context, or targeted read tools"
if self.task_context_available
else "docforge_get_context or targeted read tools"
),
"make and verify one coherent implementation slice",
"docforge_sync",
]
@ -582,8 +597,17 @@ class DocForgeService:
"compiler-AST, or function-Logic extraction"
),
)
if descriptor.profiles:
if self.task_context_available:
recommended_first_operation: dict[str, object] = {
"tool": "docforge_get_task_context",
"arguments": {
"task_kind": "implementation",
"task": "<describe the current task>",
},
"reason": "Begin with one bounded task-shaped context capsule.",
}
elif descriptor.profiles:
recommended_first_operation = {
"tool": "docforge_get_context",
"arguments": {"profile": descriptor.profiles[0].profile_id},
"reason": "Begin with one configured bounded context profile.",
@ -831,6 +855,210 @@ class DocForgeService:
operation_name="mcp.context",
)
def task_context(
self,
task_kind: TaskKind,
task: str,
*,
focus_node_id: str | None = None,
budget: int | None = None,
limit: int | None = None,
cursor: str | None = None,
) -> dict[str, Any]:
"""Return one task-shaped capsule from a single immutable graph generation."""
if not self.task_context_available:
def unavailable() -> dict[str, object]:
raise DocForgeError(
"task_context_unavailable",
(
"This binding uses a custom context provider; "
"core task planning is unavailable"
),
)
return self.invoke(
unavailable,
synchronize=False,
load_error_identity=False,
operation_name="mcp.task_context",
)
def operation() -> dict[str, object]:
maximum_evidence = min(
self.project.descriptor.limits.max_results,
MAX_TASK_EVIDENCE,
)
selected_limit = page_limit(
limit,
default=min(20, maximum_evidence),
maximum=maximum_evidence,
)
plan = build_retrieval_plan(
self.project.descriptor,
task_kind=task_kind,
task=task,
focus_node_id=focus_node_id,
budget=budget,
limit=maximum_evidence,
effective_policy=self.policy.as_dict(),
)
result = self.index.task_context(plan)
return self._page_task_context_result(
result,
selected_limit=selected_limit,
cursor=cursor,
)
return self.invoke(operation, operation_name="mcp.task_context")
def _page_task_context_result(
self,
result: dict[str, object],
*,
selected_limit: int,
cursor: str | None,
) -> dict[str, object]:
capsule_value = result.get("capsule")
if not isinstance(capsule_value, Mapping):
raise DocForgeError(
"invalid_task_context_result",
"Task context did not return a versioned capsule",
)
capsule = dict(cast(Mapping[str, object], capsule_value))
plan_value = capsule.get("plan")
generation_value = capsule.get("generation")
evidence_value = capsule.get("evidence")
omissions_value = capsule.get("omissions")
gaps_value = capsule.get("gaps")
if (
capsule.get("schema_version") != 1
or not isinstance(plan_value, Mapping)
or not isinstance(generation_value, Mapping)
or not isinstance(evidence_value, list)
or not isinstance(omissions_value, list)
or not isinstance(gaps_value, list)
or not isinstance(capsule.get("collection_hash"), str)
or not isinstance(capsule.get("capsule_hash"), str)
):
raise DocForgeError(
"invalid_task_context_result",
"Task context capsule is malformed",
)
plan_payload = cast(Mapping[str, object], plan_value)
generation = cast(Mapping[str, object], generation_value)
evidence = cast(list[object], evidence_value)
omissions = cast(list[object], omissions_value)
gaps = cast(list[object], gaps_value)
binding = {
"project_id": result.get("project_id"),
"project_root_fingerprint": result.get("project_root_fingerprint"),
"adapter": result.get("adapter"),
"revision": result.get("revision"),
"source_hash": result.get("source_hash"),
"index_schema_version": generation.get("index_schema_version"),
"effective_policy_hash": plan_payload.get("effective_policy_hash"),
"request_hash": plan_payload.get("request_hash"),
"plan_hash": plan_payload.get("plan_hash"),
"collection_hash": capsule["collection_hash"],
"capsule_hash": capsule["capsule_hash"],
}
items = [
*(("evidence", item) for item in evidence),
*(("omission", item) for item in omissions),
]
position = decode_cursor(
cursor,
kind="task-context.items",
binding=binding,
total_count=len(items),
)
page_evidence: list[object] = []
page_omissions: list[object] = []
consumed = 0
response_limited = False
maximum = self.project.descriptor.limits.max_tool_output_chars
def page_result() -> dict[str, object]:
pagination = page_receipt(
kind="task-context.items",
binding=binding,
position=position,
count=consumed,
limit=selected_limit,
total_count=len(items),
)
page_state = "incomplete" if response_limited else capsule.get("state")
page_summary = {
**cast(dict[str, object], capsule.get("summary", {})),
"page_evidence_count": len(page_evidence),
"page_omission_count": len(page_omissions),
"page_item_count": consumed,
}
page_hash = canonical_hash(
{
"capsule_hash": capsule["capsule_hash"],
"position": position,
"page_state": page_state,
"pagination": pagination,
"summary": page_summary,
"evidence": page_evidence,
"gaps": gaps,
"omissions": page_omissions,
}
)
page_capsule = {
**capsule,
"evidence": page_evidence,
"omissions": page_omissions,
"page_state": page_state,
"page_hash": page_hash,
"pagination": pagination,
"summary": page_summary,
}
return {
**result,
"capsule": page_capsule,
"next_cursor": pagination["next_cursor"],
"pagination": pagination,
}
for kind, item in items[position:]:
if consumed >= selected_limit:
break
destination = page_evidence if kind == "evidence" else page_omissions
destination.append(item)
consumed += 1
decorated = {
**page_result(),
"server_version": SERVER_VERSION,
"content_warning": CONTENT_WARNING,
"staleness": "current",
}
if self._encoded_length(decorated) <= maximum:
continue
destination.pop()
consumed -= 1
response_limited = True
if consumed == 0:
subject = "unknown"
if isinstance(item, Mapping):
item_payload = cast(Mapping[str, object], item)
candidate = item_payload.get("node_id") or item_payload.get("subject")
if isinstance(candidate, str) and candidate:
subject = candidate[:256]
page_omissions.append(
{
"code": "response_limit",
"subject": subject,
"detail_hash": canonical_hash(cast(object, item)),
}
)
consumed = 1
break
return page_result()
def _page_context_result(
self,
result: dict[str, object],
@ -1151,6 +1379,26 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
return service.context(profile, budget, limit=limit, cursor=cursor)
@server.tool(name="docforge_get_task_context")
def get_task_context(
task_kind: TaskKind,
task: str,
focus_node_id: str | None = None,
budget: int | None = None,
limit: int | None = None,
cursor: str | None = None,
) -> dict[str, Any]:
"""Return one bounded task-shaped context capsule with explicit evidence gaps."""
return service.task_context(
task_kind,
task,
focus_node_id=focus_node_id,
budget=budget,
limit=limit,
cursor=cursor,
)
@server.tool(name="docforge_validate_project")
def validate_project() -> dict[str, Any]:
"""Validate current canonical sources and graph without writing any project file."""
@ -1201,6 +1449,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
dependencies,
impact,
get_context,
get_task_context,
validate_project,
render_status,
visualize,

671
src/docforge/retrieval.py Normal file
View file

@ -0,0 +1,671 @@
"""Versioned task-shaped retrieval plans and immutable context capsules."""
from __future__ import annotations
from dataclasses import dataclass, replace
from typing import Literal, cast
from .errors import DocForgeError
from .models import Edge, Node, ProjectDescriptor
from .pagination import canonical_hash
TaskKind = Literal[
"change",
"implementation",
"failure",
"ownership",
"test",
"operation",
"release",
]
TASK_KINDS: tuple[TaskKind, ...] = (
"change",
"implementation",
"failure",
"ownership",
"test",
"operation",
"release",
)
RelationCategory = Literal[
"structure",
"implementation",
"dependency",
"execution",
"data",
"evidence",
"context",
"unclassified",
]
BASE_RELATION_CATEGORIES: tuple[RelationCategory, ...] = (
"structure",
"implementation",
"dependency",
"execution",
"data",
"evidence",
"context",
)
RELATION_CATEGORIES: dict[RelationCategory, tuple[str, ...]] = {
"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": (),
}
TASK_REQUIREMENTS: dict[TaskKind, tuple[RelationCategory, ...]] = {
"change": ("dependency",),
"implementation": ("implementation",),
"failure": ("execution",),
"ownership": ("structure",),
"test": ("evidence",),
"operation": ("execution",),
"release": ("evidence",),
}
PLANNER_ID = "docforge.core.task-context"
PLANNER_VERSION = 1
MAX_TASK_EVIDENCE = 1_000
MAX_TASK_CANDIDATE_EDGES = 100_000
MAX_TASK_QUERY_CHARS = 10_000
PROVENANCE_LIMITATIONS = (
"evidence_type_unavailable",
"extractor_identity_unavailable",
"relationship_provenance_unavailable",
"observation_time_unavailable",
)
_RELATION_TO_CATEGORY = {
relation: category
for category, relations in RELATION_CATEGORIES.items()
for relation in relations
}
def relation_category(relation: str) -> RelationCategory:
"""Classify only versioned known aliases; preserve every other relation."""
return cast(RelationCategory, _RELATION_TO_CATEGORY.get(relation, "unclassified"))
@dataclass(frozen=True)
class RetrievalStepV1:
step_id: str
operation: Literal["exact", "search", "outgoing", "incoming", "metadata"]
relation_scope: Literal["none", "project_allowed"]
relation_set_hash: str | None
direction: Literal["none", "outgoing", "incoming"]
depth: int
limit: int
required: bool
evidence_role: str
def as_dict(self) -> dict[str, object]:
return {
"step_id": self.step_id,
"operation": self.operation,
"relation_scope": self.relation_scope,
"relation_set_hash": self.relation_set_hash,
"direction": self.direction,
"depth": self.depth,
"limit": self.limit,
"required": self.required,
"evidence_role": self.evidence_role,
}
@dataclass(frozen=True)
class RetrievalRequirementV1:
requirement_id: str
category: RelationCategory
def as_dict(self) -> dict[str, object]:
return {
"requirement_id": self.requirement_id,
"check": "selected_relation_category",
"category": self.category,
"required": True,
}
@dataclass(frozen=True)
class RetrievalPlanV1:
schema_version: Literal[1]
planner_id: str
planner_version: int
task_kind: TaskKind
task_query: str
focus_node_id: str | None
request_hash: str
effective_policy_hash: str
max_evidence: int
max_tokens: int
max_depth: int
max_candidate_edges: int
category_order: tuple[RelationCategory, ...]
steps: tuple[RetrievalStepV1, ...]
requirements: tuple[RetrievalRequirementV1, ...]
plan_hash: str
def as_dict(self) -> dict[str, object]:
return self.payload(include_hash=True)
def payload(self, *, include_hash: bool) -> dict[str, object]:
result: dict[str, object] = {
"schema_version": self.schema_version,
"planner": {
"id": self.planner_id,
"version": self.planner_version,
},
"task_kind": self.task_kind,
"request_hash": self.request_hash,
"effective_policy_hash": self.effective_policy_hash,
"focus_node_id": self.focus_node_id,
"limits": {
"max_evidence": self.max_evidence,
"max_tokens": self.max_tokens,
"max_depth": self.max_depth,
"max_candidate_edges": self.max_candidate_edges,
},
"category_order": list(self.category_order),
"steps": [step.as_dict() for step in self.steps],
"requirements": [requirement.as_dict() for requirement in self.requirements],
}
if include_hash:
result["plan_hash"] = self.plan_hash
return result
def build_retrieval_plan(
descriptor: ProjectDescriptor,
*,
task_kind: str,
task: str,
focus_node_id: str | None,
budget: int | None,
limit: int | None,
effective_policy: dict[str, object],
) -> RetrievalPlanV1:
"""Derive one fixed plan from bounded inputs rather than accepting caller operations."""
return _build_retrieval_plan(
descriptor,
task_kind=task_kind,
task=task,
focus_node_id=focus_node_id,
budget=budget,
limit=limit,
effective_policy_hash=canonical_hash(effective_policy),
)
def validate_retrieval_plan(
plan: RetrievalPlanV1,
descriptor: ProjectDescriptor,
) -> RetrievalPlanV1:
"""Reject forged, stale-shape, or internally inconsistent public plan objects."""
try:
expected = _build_retrieval_plan(
descriptor,
task_kind=plan.task_kind,
task=plan.task_query,
focus_node_id=plan.focus_node_id,
budget=plan.max_tokens,
limit=plan.max_evidence,
effective_policy_hash=plan.effective_policy_hash,
)
except (AttributeError, TypeError, DocForgeError) as error:
raise DocForgeError(
"invalid_retrieval_plan",
"Task retrieval plan is malformed or outside the fixed version-1 contract",
) from error
if plan != expected:
raise DocForgeError(
"invalid_retrieval_plan",
"Task retrieval plan does not match its fixed version-1 derivation",
)
return plan
def _build_retrieval_plan(
descriptor: ProjectDescriptor,
*,
task_kind: str,
task: str,
focus_node_id: str | None,
budget: int | None,
limit: int | None,
effective_policy_hash: str,
) -> RetrievalPlanV1:
if task_kind not in TASK_KINDS:
raise DocForgeError(
"invalid_task_kind",
"Task context kind is unsupported",
task_kind=task_kind,
allowed=list(TASK_KINDS),
)
selected_kind: TaskKind = task_kind # type: ignore[assignment]
normalized_task = task.strip()
if not normalized_task or len(normalized_task) > min(
descriptor.limits.max_query_chars,
MAX_TASK_QUERY_CHARS,
):
raise DocForgeError(
"invalid_task_focus",
"Task description is empty or exceeds the configured query limit",
)
if focus_node_id is not None and (not focus_node_id or len(focus_node_id) > 256):
raise DocForgeError("invalid_task_focus", "Task focus node ID is invalid")
selected_budget = _bounded_value(
budget,
default=min(8_000, descriptor.limits.max_context_tokens),
maximum=descriptor.limits.max_context_tokens,
code="invalid_budget",
)
selected_limit = _bounded_value(
limit,
default=min(20, descriptor.limits.max_results),
maximum=min(descriptor.limits.max_results, MAX_TASK_EVIDENCE),
code="invalid_limit",
)
if not _is_sha256(effective_policy_hash):
raise DocForgeError(
"invalid_retrieval_plan",
"Effective policy identity is not a SHA-256 value",
)
selected_depth = min(2, descriptor.limits.max_traversal_depth)
requirements = tuple(
RetrievalRequirementV1(
requirement_id=f"{selected_kind}.{category}",
category=category,
)
for category in TASK_REQUIREMENTS[selected_kind]
)
category_order: tuple[RelationCategory, ...] = (
*TASK_REQUIREMENTS[selected_kind],
*(
category
for category in BASE_RELATION_CATEGORIES
if category not in TASK_REQUIREMENTS[selected_kind]
),
"unclassified",
)
relation_set_hash = canonical_hash(sorted(descriptor.allowed_relations))
focus_operation: Literal["exact", "search"] = "exact" if focus_node_id else "search"
steps = (
RetrievalStepV1(
step_id="focus",
operation=focus_operation,
relation_scope="none",
relation_set_hash=None,
direction="none",
depth=0,
limit=1,
required=True,
evidence_role="focus",
),
RetrievalStepV1(
step_id="outgoing",
operation="outgoing",
relation_scope="project_allowed",
relation_set_hash=relation_set_hash,
direction="outgoing",
depth=selected_depth,
limit=selected_limit,
required=False,
evidence_role="related",
),
RetrievalStepV1(
step_id="incoming",
operation="incoming",
relation_scope="project_allowed",
relation_set_hash=relation_set_hash,
direction="incoming",
depth=selected_depth,
limit=selected_limit,
required=False,
evidence_role="related",
),
RetrievalStepV1(
step_id="metadata",
operation="metadata",
relation_scope="none",
relation_set_hash=None,
direction="none",
depth=0,
limit=selected_limit,
required=True,
evidence_role="provenance",
),
)
request_hash = canonical_hash(
{
"task_kind": selected_kind,
"task": normalized_task,
"focus_node_id": focus_node_id,
"budget": selected_budget,
"limit": selected_limit,
}
)
placeholder = RetrievalPlanV1(
schema_version=1,
planner_id=PLANNER_ID,
planner_version=PLANNER_VERSION,
task_kind=selected_kind,
task_query=normalized_task,
focus_node_id=focus_node_id,
request_hash=request_hash,
effective_policy_hash=effective_policy_hash,
max_evidence=selected_limit,
max_tokens=selected_budget,
max_depth=selected_depth,
max_candidate_edges=min(
(selected_limit + 1) ** 2,
MAX_TASK_CANDIDATE_EDGES,
),
category_order=category_order,
steps=steps,
requirements=requirements,
plan_hash="",
)
return replace(
placeholder,
plan_hash=canonical_hash(placeholder.payload(include_hash=False)),
)
@dataclass(frozen=True)
class CapsuleRelationshipV1:
source_id: str
relation: str
target_id: str
direction: Literal["outgoing", "incoming"]
category: RelationCategory
def as_dict(self) -> dict[str, object]:
return {
"source_id": self.source_id,
"relation": self.relation,
"target_id": self.target_id,
"direction": self.direction,
"category": self.category,
"provenance": "validated_graph_edge_without_source_provenance",
}
@dataclass(frozen=True)
class CapsuleEvidenceV1:
evidence_hash: str
role: Literal["focus", "related"]
reason_code: Literal["exact_focus", "lexical_focus", "relationship_path"]
node: Node
depth: int
relationship_path: tuple[CapsuleRelationshipV1, ...]
relationship_reasons: tuple[CapsuleRelationshipV1, ...]
estimated_tokens: int
def as_dict(self) -> dict[str, object]:
result = self.payload()
return {"evidence_hash": self.evidence_hash, **result}
def payload(self) -> dict[str, object]:
result: dict[str, object] = {
"role": self.role,
"reason_code": self.reason_code,
"node_id": self.node.node_id,
"title": self.node.title,
"family": self.node.family,
"authority": self.node.authority,
"status": self.node.status,
"tags": list(self.node.tags),
"summary": self.node.summary,
"text": _node_text(self.node),
"estimated_tokens": self.estimated_tokens,
"source": {
"path": self.node.source_path,
"anchor": self.node.source_anchor,
"content_hash": self.node.content_hash,
},
"depth": self.depth,
"relationship_path": [
relationship.as_dict() for relationship in self.relationship_path
],
"relationship_reasons": [
relationship.as_dict() for relationship in self.relationship_reasons
],
"provenance_limitations": list(PROVENANCE_LIMITATIONS),
}
return result
def capsule_evidence(
*,
role: Literal["focus", "related"],
reason_code: Literal["exact_focus", "lexical_focus", "relationship_path"],
node: Node,
depth: int,
relationship_path: tuple[CapsuleRelationshipV1, ...],
relationship_reasons: tuple[CapsuleRelationshipV1, ...],
) -> CapsuleEvidenceV1:
tokens = estimate_tokens(_node_text(node))
placeholder = CapsuleEvidenceV1(
evidence_hash="",
role=role,
reason_code=reason_code,
node=node,
depth=depth,
relationship_path=relationship_path,
relationship_reasons=relationship_reasons,
estimated_tokens=tokens,
)
return replace(
placeholder,
evidence_hash=canonical_hash(placeholder.payload()),
)
@dataclass(frozen=True)
class EvidenceGapV1:
code: Literal[
"focus_not_found",
"focus_ambiguous",
"category_not_declared",
"no_selected_evidence",
"evidence_incomplete",
"unclassified_relation",
]
requirement_id: str
category: RelationCategory | None
state: Literal["missing", "incomplete", "blocked", "limitation"]
check_complete: bool
detail: str
def as_dict(self) -> dict[str, object]:
return {
"code": self.code,
"requirement_id": self.requirement_id,
"category": self.category,
"state": self.state,
"check_complete": self.check_complete,
"detail": self.detail,
}
@dataclass(frozen=True)
class CapsuleOmissionV1:
code: Literal[
"result_limit",
"token_budget",
"response_limit",
"edge_examination_limit",
"unclassified_relation_limit",
]
subject: str
detail_hash: str
def as_dict(self) -> dict[str, str]:
return {
"code": self.code,
"subject": self.subject,
"detail_hash": self.detail_hash,
}
@dataclass(frozen=True)
class ContextCapsuleV1:
schema_version: Literal[1]
state: Literal["complete", "incomplete", "blocked"]
task_kind: TaskKind
generation: tuple[tuple[str, object], ...]
plan: RetrievalPlanV1
focus_state: Literal["resolved", "not_found", "ambiguous"]
focus_node_id: str | None
focus_candidate_count: int
evidence: tuple[CapsuleEvidenceV1, ...]
gaps: tuple[EvidenceGapV1, ...]
omissions: tuple[CapsuleOmissionV1, ...]
selected_count: int
examined_edge_count: int
estimated_tokens: int
unclassified_relations: tuple[str, ...]
collection_hash: str
capsule_hash: str
def as_dict(self) -> dict[str, object]:
return self.payload(include_hashes=True)
def payload(self, *, include_hashes: bool) -> dict[str, object]:
evidence = [item.as_dict() for item in self.evidence]
gaps = [gap.as_dict() for gap in self.gaps]
omissions = [omission.as_dict() for omission in self.omissions]
result: dict[str, object] = {
"schema_version": self.schema_version,
"state": self.state,
"task_kind": self.task_kind,
"generation": dict(self.generation),
"plan": self.plan.as_dict(),
"focus": {
"state": self.focus_state,
"node_id": self.focus_node_id,
"candidate_count": self.focus_candidate_count,
},
"evidence": evidence,
"gaps": gaps,
"omissions": omissions,
"summary": {
"evidence_count": len(evidence),
"gap_count": len(gaps),
"omission_count": len(omissions),
"selected_count": self.selected_count,
"examined_edge_count": self.examined_edge_count,
"estimated_tokens": self.estimated_tokens,
"unclassified_relations": list(self.unclassified_relations),
},
}
if include_hashes:
result["collection_hash"] = self.collection_hash
result["capsule_hash"] = self.capsule_hash
return result
def finalize_capsule(
*,
state: Literal["complete", "incomplete", "blocked"],
task_kind: TaskKind,
generation: dict[str, object],
plan: RetrievalPlanV1,
focus_state: Literal["resolved", "not_found", "ambiguous"],
focus_node_id: str | None,
focus_candidate_count: int,
evidence: tuple[CapsuleEvidenceV1, ...],
gaps: tuple[EvidenceGapV1, ...],
omissions: tuple[CapsuleOmissionV1, ...],
selected_count: int,
examined_edge_count: int,
estimated_tokens: int,
unclassified_relations: tuple[str, ...],
) -> ContextCapsuleV1:
collection_hash = canonical_hash(
{
"generation": generation,
"plan_hash": plan.plan_hash,
"evidence": [item.evidence_hash for item in evidence],
"gaps": [gap.as_dict() for gap in gaps],
"omissions": [omission.as_dict() for omission in omissions],
}
)
placeholder = ContextCapsuleV1(
schema_version=1,
state=state,
task_kind=task_kind,
generation=tuple(generation.items()),
plan=plan,
focus_state=focus_state,
focus_node_id=focus_node_id,
focus_candidate_count=focus_candidate_count,
evidence=evidence,
gaps=gaps,
omissions=omissions,
selected_count=selected_count,
examined_edge_count=examined_edge_count,
estimated_tokens=estimated_tokens,
unclassified_relations=unclassified_relations,
collection_hash=collection_hash,
capsule_hash="",
)
capsule_hash = canonical_hash(
{
**placeholder.payload(include_hashes=False),
"collection_hash": collection_hash,
}
)
return replace(placeholder, capsule_hash=capsule_hash)
def estimate_tokens(text: str) -> int:
return max(1, (len(text) + 3) // 4)
def edge_tuple(edge: Edge) -> tuple[str, str, str]:
return edge.source_id, edge.relation, edge.target_id
def _node_text(node: Node) -> str:
return (
f"ID: {node.node_id}\nTitle: {node.title}\nFamily: {node.family}\n"
f"Authority: {node.authority}\nStatus: {node.status}\nSource: {node.source_path}\n"
f"Summary: {node.summary}\n\n{node.content}"
)
def _bounded_value(
value: int | None,
*,
default: int,
maximum: int,
code: str,
) -> int:
selected = default if value is None else value
if type(selected) is not int or selected < 1 or selected > maximum:
raise DocForgeError(code, "Task context limit is outside the configured range")
return selected
def _is_sha256(value: object) -> bool:
return (
isinstance(value, str)
and len(value) == 64
and all(character in "0123456789abcdef" for character in value)
)

View file

@ -91,6 +91,7 @@ OPERATION_NAMES = frozenset(
"mcp.dependencies",
"mcp.impact",
"mcp.context",
"mcp.task_context",
"mcp.validate_project",
"mcp.render_status",
"mcp.visualize",