Add structured compiler diagnostics
This commit is contained in:
parent
0fe968c475
commit
24bd13f9d9
20 changed files with 1386 additions and 58 deletions
|
|
@ -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")
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue