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

Add structured compiler diagnostics

This commit is contained in:
Andraxion 2026-07-29 05:07:16 -04:00
parent 0fe968c475
commit 24bd13f9d9
20 changed files with 1386 additions and 58 deletions

View file

@ -16,9 +16,10 @@ from .changesets import ChangesetStore
from .context import compile_context
from .errors import DocForgeError
from .index import ProjectIndex
from .models import ProjectService, RuntimeValidatedProject
from .models import IncrementalStateProject, ProjectService, RuntimeValidatedProject
from .project import Project, project_root_fingerprint
from .rendering import RenderService
from .telemetry import request, stage
from .viewer_manager import ViewerManagerClient
SERVER_VERSION = "1.3.0.dev0"
@ -125,6 +126,7 @@ class DocForgeService:
tool_surface: tuple[str, ...] | None = None,
binding_metadata: Mapping[str, object] | None = None,
no_ast: bool = False,
diagnostics: bool = False,
) -> None:
self.project = project
self.index = ProjectIndex(self.project, allow_logic=not no_ast)
@ -140,6 +142,7 @@ class DocForgeService:
self.context_provider = context_provider
self.binding_metadata = dict(binding_metadata or {})
self.no_ast = no_ast
self.diagnostics = diagnostics
self.tool_surface = tool_surface or (
*ALL_TOOLS,
*(APPLICATION_TOOLS if self.application.enabled else ()),
@ -177,6 +180,29 @@ class DocForgeService:
synchronize: bool = True,
mutation: _MutationPolicy | None = None,
load_error_identity: bool = True,
operation_name: str = "mcp.invoke",
) -> dict[str, Any]:
with request(operation_name, enabled=self.diagnostics) as collector:
result = self._invoke_core(
operation,
synchronize=synchronize,
mutation=mutation,
load_error_identity=load_error_identity,
)
if collector is None:
return result
diagnostics = collector.as_dict(outcome="ok" if result.get("status") == "ok" else "error")
with_diagnostics = {**result, "diagnostics": diagnostics}
maximum = self.project.descriptor.limits.max_tool_output_chars
return with_diagnostics if self._encoded_length(with_diagnostics) <= maximum else result
def _invoke_core(
self,
operation: Callable[[], dict[str, object]],
*,
synchronize: bool = True,
mutation: _MutationPolicy | None = None,
load_error_identity: bool = True,
) -> dict[str, Any]:
synchronization: dict[str, object] | None = None
maximum = self.project.descriptor.limits.max_tool_output_chars
@ -192,7 +218,8 @@ class DocForgeService:
try:
try:
if isinstance(self.project, RuntimeValidatedProject):
self.project.validate_runtime()
with stage("mcp.runtime_validation"):
self.project.validate_runtime()
result: dict[str, Any] = operation()
except DocForgeError as error:
if not synchronize or error.code not in RECOVERABLE_INDEX_ERROR_CODES:
@ -214,13 +241,26 @@ class DocForgeService:
}
if load_error_identity:
try:
snapshot = self.project.load()
result.update(
{
"revision": snapshot.revision,
"source_hash": snapshot.source_hash,
}
state = (
self.project.incremental_state()
if isinstance(self.project, IncrementalStateProject)
else None
)
if state is not None:
result.update(
{
"revision": state.revision,
"source_hash": state.source_hash,
}
)
else:
snapshot = self.project.load()
result.update(
{
"revision": snapshot.revision,
"source_hash": snapshot.source_hash,
}
)
except DocForgeError:
result.update({"revision": "unknown", "source_hash": None})
else:
@ -451,7 +491,11 @@ class DocForgeService:
return None
def synchronize(self) -> dict[str, object]:
return self.invoke(self.index.synchronize, synchronize=False)
return self.invoke(
self.index.synchronize,
synchronize=False,
operation_name="mcp.sync",
)
def bootstrap(self) -> dict[str, object]:
def operation() -> dict[str, object]:
@ -506,6 +550,7 @@ class DocForgeService:
operation,
synchronize=False,
load_error_identity=False,
operation_name="mcp.bootstrap",
)
def project_info(self) -> dict[str, object]:
@ -533,7 +578,7 @@ class DocForgeService:
"index_health": index_health,
}
return self.invoke(operation)
return self.invoke(operation, operation_name="mcp.project_info")
def contract(self) -> dict[str, object]:
def operation() -> dict[str, object]:
@ -602,7 +647,7 @@ class DocForgeService:
"project_switching_allowed": False,
}
return self.invoke(operation)
return self.invoke(operation, operation_name="mcp.contract")
def get_logic(self, owner_node_id: str) -> dict[str, object]:
"""Return one Logic projection unless the binding preserves a no-AST adapter."""
@ -618,8 +663,15 @@ class DocForgeService:
),
)
return self.invoke(forbidden, synchronize=False)
return self.invoke(lambda: self.index.get_logic(owner_node_id))
return self.invoke(
forbidden,
synchronize=False,
operation_name="mcp.get_logic",
)
return self.invoke(
lambda: self.index.get_logic(owner_node_id),
operation_name="mcp.get_logic",
)
def validate_project(self) -> dict[str, object]:
def operation() -> dict[str, object]:
@ -635,7 +687,7 @@ class DocForgeService:
"edge_count": len(snapshot.edges),
}
return self.invoke(operation)
return self.invoke(operation, operation_name="mcp.validate_project")
def render_status(
self,
@ -652,10 +704,14 @@ class DocForgeService:
operation,
synchronize=False,
load_error_identity=False,
operation_name="mcp.render_status",
)
def context(self, profile: str, budget: int | None = None) -> dict[str, Any]:
return self.invoke(lambda: self.context_provider(self.index, profile, budget))
return self.invoke(
lambda: self.context_provider(self.index, profile, budget),
operation_name="mcp.context",
)
def visualize(
self,
@ -680,13 +736,21 @@ class DocForgeService:
"visualization": visualization,
}
return self.invoke(operation)
return self.invoke(operation, operation_name="mcp.visualize")
def stop_visualization(self) -> dict[str, object]:
return self.invoke(self.visualization.stop)
return self.invoke(
self.visualization.stop,
operation_name="mcp.stop_visualization",
)
def visualization_status(self) -> dict[str, object]:
return self.invoke(self.visualization.status)
return self.invoke(
self.visualization.status,
synchronize=False,
load_error_identity=False,
operation_name="mcp.visualization_status",
)
def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMCP:
@ -752,7 +816,10 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
def get_node(node_id: str) -> dict[str, Any]:
"""Return one exact stable node from the current validated project index."""
return service.invoke(lambda: service.index.get_node(node_id))
return service.invoke(
lambda: service.index.get_node(node_id),
operation_name="mcp.get_node",
)
@server.tool(name="docforge_get_logic")
def get_logic(owner_node_id: str) -> dict[str, Any]:
@ -764,7 +831,10 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
def search(query: str, limit: int | None = None) -> dict[str, Any]:
"""Run bounded lexical search over the current validated project index."""
return service.invoke(lambda: service.index.search(query, limit=limit))
return service.invoke(
lambda: service.index.search(query, limit=limit),
operation_name="mcp.search",
)
@server.tool(name="docforge_filter_nodes")
def filter_nodes(
@ -783,7 +853,8 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
status=status,
tag=tag,
limit=limit,
)
),
operation_name="mcp.filter",
)
@server.tool(name="docforge_backlinks")
@ -795,7 +866,8 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
"""Return bounded incoming relationships for one exact stable node."""
return service.invoke(
lambda: service.index.backlinks(node_id, relation=relation, limit=limit)
lambda: service.index.backlinks(node_id, relation=relation, limit=limit),
operation_name="mcp.backlinks",
)
@server.tool(name="docforge_dependencies")
@ -806,7 +878,10 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
) -> dict[str, Any]:
"""Traverse declared depends_on relationships within the configured depth limit."""
return service.invoke(lambda: service.index.dependencies(node_id, depth=depth, limit=limit))
return service.invoke(
lambda: service.index.dependencies(node_id, depth=depth, limit=limit),
operation_name="mcp.dependencies",
)
@server.tool(name="docforge_impact")
def impact(
@ -816,7 +891,10 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
) -> dict[str, Any]:
"""Traverse bounded incoming relationships and report exact paths."""
return service.invoke(lambda: service.index.impact(node_id, depth=depth, limit=limit))
return service.invoke(
lambda: service.index.impact(node_id, depth=depth, limit=limit),
operation_name="mcp.impact",
)
@server.tool(name="docforge_get_context")
def get_context(profile: str, budget: int | None = None) -> dict[str, Any]:
@ -890,6 +968,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
return service.invoke(
lambda: service.changesets.create(changeset_id),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.create",
"changeset",
@ -908,6 +987,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
return service.invoke(
lambda: service.changesets.register(changeset_id, operations),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.register",
"changeset",
@ -927,14 +1007,18 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
lambda: service.changesets.list_changesets(
include_history=include_history,
status=status,
)
),
operation_name="mcp.changeset",
)
@server.tool(name="docforge_get_changeset")
def get_changeset(changeset_id: str) -> dict[str, Any]:
"""Inspect a stored proposal even when its canonical base has become stale."""
return service.invoke(lambda: service.changesets.inspect(changeset_id))
return service.invoke(
lambda: service.changesets.inspect(changeset_id),
operation_name="mcp.changeset",
)
@server.tool(name="docforge_rebase_changeset")
def rebase_changeset(
@ -949,6 +1033,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
expected_changeset_hash,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.rebase",
"changeset",
@ -972,6 +1057,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
reason,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.abandon",
"changeset",
@ -1005,6 +1091,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
rationale=rationale,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.append_create",
"changeset",
@ -1038,6 +1125,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
rationale=rationale,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.append_update",
"changeset",
@ -1067,6 +1155,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
rationale=rationale,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.append_move",
"changeset",
@ -1096,6 +1185,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
rationale=rationale,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.append_relationship_update",
"changeset",
@ -1125,6 +1215,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
rationale=rationale,
),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.append_delete",
"changeset",
@ -1137,13 +1228,19 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
def validate_changeset(changeset_id: str) -> dict[str, Any]:
"""Validate a proposal against its exact canonical base and other active proposals."""
return service.invoke(lambda: service.changesets.validate(changeset_id))
return service.invoke(
lambda: service.changesets.validate(changeset_id),
operation_name="mcp.changeset",
)
@server.tool(name="docforge_get_changeset_diff")
def get_changeset_diff(changeset_id: str) -> dict[str, Any]:
"""Return a deterministic structured and textual diff without applying the proposal."""
return service.invoke(lambda: service.changesets.diff(changeset_id))
return service.invoke(
lambda: service.changesets.diff(changeset_id),
operation_name="mcp.changeset",
)
@server.tool(name="docforge_preview_changeset")
def preview_changeset(changeset_id: str, view_id: str) -> dict[str, Any]:
@ -1152,6 +1249,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
return service.invoke(
lambda: service.rendering.preview(changeset_id, view_id),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"render.preview",
"preview",
@ -1188,6 +1286,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
return service.invoke(
lambda: service.application.apply(changeset_id, expected_changeset_hash),
synchronize=False,
operation_name="mcp.mutation",
mutation=service.mutation(
"changeset.apply",
"application",
@ -1206,6 +1305,7 @@ def create_server(
*,
canonical_applier_id: str | None = None,
no_ast: bool = False,
diagnostics: bool = False,
) -> FastMCP:
project = Project.open(project_root)
return create_project_server(
@ -1220,6 +1320,7 @@ def create_server(
"adapter_mode": "generic",
},
no_ast=no_ast,
diagnostics=diagnostics,
)
@ -1232,6 +1333,7 @@ def create_project_server(
context_provider: ContextProvider = compile_context,
binding_metadata: Mapping[str, object] | None = None,
no_ast: bool = False,
diagnostics: bool = False,
) -> FastMCP:
"""Create the full fixed MCP surface for one explicitly configured project service."""
@ -1243,6 +1345,7 @@ def create_project_server(
context_provider=context_provider,
binding_metadata=binding_metadata,
no_ast=no_ast,
diagnostics=diagnostics,
)
return _create_bound_server(service, read_only=False)
@ -1253,6 +1356,7 @@ def create_read_only_server(
context_provider: ContextProvider = compile_context,
binding_metadata: Mapping[str, object] | None = None,
no_ast: bool = False,
diagnostics: bool = False,
) -> FastMCP:
"""Create an adapter-capable MCP server exposing only the fixed read tool surface."""
@ -1262,6 +1366,7 @@ def create_read_only_server(
tool_surface=READ_TOOLS,
binding_metadata=binding_metadata,
no_ast=no_ast,
diagnostics=diagnostics,
)
return _create_bound_server(service, read_only=True)
@ -1279,12 +1384,18 @@ def main() -> None:
"and function-Logic extraction changes"
),
)
parser.add_argument(
"--diagnostics",
action="store_true",
help="Attach bounded request-local stage timings and counters",
)
arguments = parser.parse_args()
create_server(
arguments.project_root,
arguments.proposal_writer,
canonical_applier_id=arguments.canonical_applier,
no_ast=arguments.no_ast,
diagnostics=arguments.diagnostics,
).run(transport="stdio")