Preserve no-AST adapter bindings
This commit is contained in:
parent
bb13258861
commit
6c05607b14
7 changed files with 209 additions and 13 deletions
|
|
@ -40,6 +40,12 @@ path without modification. Adapters gain incremental performance only when they
|
||||||
implement the source manifest and extraction methods. Incremental adapters must retain
|
implement the source manifest and extraction methods. Incremental adapters must retain
|
||||||
`load_projection()` as their clean-rebuild fallback and equivalence oracle.
|
`load_projection()` as their clean-rebuild fallback and equivalence oracle.
|
||||||
|
|
||||||
|
MCP bindings can additionally select `--no-ast` when an owner wants to preserve an existing
|
||||||
|
non-AST adapter. The binding advertises that policy to clients, forbids adapter rewrites that add
|
||||||
|
AST, Tree-sitter, compiler-AST, or function-Logic extraction, blocks the Logic tool, and rejects
|
||||||
|
nonempty Logic publication. Complete-projection adapters continue unchanged, and non-AST
|
||||||
|
incremental fingerprinting and caching remain allowed.
|
||||||
|
|
||||||
## Graph views
|
## Graph views
|
||||||
|
|
||||||
The browser presents the primary architecture graph through three complementary views and loads a
|
The browser presents the primary architecture graph through three complementary views and loads a
|
||||||
|
|
|
||||||
|
|
@ -162,3 +162,32 @@ does not expose canonical application.
|
||||||
|
|
||||||
DocForge pins the official stable Python MCP SDK to the compatible `mcp>=1.28,<2` release line.
|
DocForge pins the official stable Python MCP SDK to the compatible `mcp>=1.28,<2` release line.
|
||||||
Migration to a later major release requires a separate contract and protocol compatibility review.
|
Migration to a later major release requires a separate contract and protocol compatibility review.
|
||||||
|
|
||||||
|
## Preserved no-AST bindings
|
||||||
|
|
||||||
|
An owner may start the generic MCP server with `--no-ast`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docforge-mcp --project-root /absolute/project --no-ast
|
||||||
|
```
|
||||||
|
|
||||||
|
Project-owned integrations select the same immutable process policy with
|
||||||
|
`create_project_server(..., no_ast=True)` or `create_read_only_server(..., no_ast=True)`.
|
||||||
|
|
||||||
|
The policy preserves the current adapter extraction strategy. It does not require an adapter API
|
||||||
|
migration and does not disable complete-projection loading or non-AST incremental fingerprinting,
|
||||||
|
invalidation, and caching.
|
||||||
|
|
||||||
|
Both `docforge_bootstrap` and `docforge_get_contract` report the exact policy. MCP server
|
||||||
|
instructions tell clients not to add Python AST, Tree-sitter, compiler-AST, or function-Logic
|
||||||
|
extraction. Under this binding:
|
||||||
|
|
||||||
|
- `docforge_get_logic` returns `adapter_policy_forbids_logic`;
|
||||||
|
- a nonempty Logic projection is rejected before index publication;
|
||||||
|
- `adapter_ast_upgrade` and `function_logic_extraction` appear as excluded operations; and
|
||||||
|
- changing the policy requires changing the process configuration and starting a new MCP process.
|
||||||
|
|
||||||
|
The policy governs the DocForge binding and conforming MCP clients. DocForge still exposes no
|
||||||
|
filesystem sandbox and cannot prevent an unrelated process with direct repository write access
|
||||||
|
from editing adapter files. Repository permissions and project instructions remain responsible for
|
||||||
|
that broader boundary.
|
||||||
|
|
|
||||||
|
|
@ -656,6 +656,25 @@ traversal.
|
||||||
See [Incremental Adapter Indexing](INCREMENTAL_INDEXING.md) for the complete contract, cache
|
See [Incremental Adapter Indexing](INCREMENTAL_INDEXING.md) for the complete contract, cache
|
||||||
invalidation rules, manual-application lifecycle, and lazy Logic boundary.
|
invalidation rules, manual-application lifecycle, and lazy Logic boundary.
|
||||||
|
|
||||||
|
### Preserving an older non-AST adapter
|
||||||
|
|
||||||
|
Use `--no-ast` on the MCP binding when the project owner wants the existing adapter preserved
|
||||||
|
without AST, Tree-sitter, compiler-AST, or function-Logic upgrades:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docforge-mcp --project-root /absolute/project --no-ast
|
||||||
|
```
|
||||||
|
|
||||||
|
For a project-owned server, pass `no_ast=True` to `create_project_server()` or
|
||||||
|
`create_read_only_server()`. Bootstrap and contract responses then expose
|
||||||
|
`mode=preserve-no-ast`. The Logic tool is blocked, and DocForge refuses to publish nonempty Logic
|
||||||
|
projections.
|
||||||
|
|
||||||
|
This policy does not disable the Release 1 `load_projection()` path. It also permits incremental
|
||||||
|
fingerprinting and caching when those mechanisms do not add AST analysis. The adapter can
|
||||||
|
therefore benefit from current synchronization, proposals, application, rendering, and graph tools
|
||||||
|
without a source-analysis rewrite.
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### `stale_index` or `visualization_stale`
|
### `stale_index` or `visualization_stale`
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,9 @@ def _status(
|
||||||
class ProjectIndex:
|
class ProjectIndex:
|
||||||
"""A disposable index that always checks current canonical source before queries."""
|
"""A disposable index that always checks current canonical source before queries."""
|
||||||
|
|
||||||
def __init__(self, project: ProjectService) -> None:
|
def __init__(self, project: ProjectService, *, allow_logic: bool = True) -> None:
|
||||||
self.project = project
|
self.project = project
|
||||||
|
self.allow_logic = allow_logic
|
||||||
self._verified_index_signature: tuple[int, int, int, int, int] | None = None
|
self._verified_index_signature: tuple[int, int, int, int, int] | None = None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
|
@ -391,7 +392,16 @@ class ProjectIndex:
|
||||||
|
|
||||||
def _logic_projections(self) -> tuple[LogicProjection, ...]:
|
def _logic_projections(self) -> tuple[LogicProjection, ...]:
|
||||||
if isinstance(self.project, LogicProject):
|
if isinstance(self.project, LogicProject):
|
||||||
return self.project.logic_projections()
|
projections = self.project.logic_projections()
|
||||||
|
if projections and not self.allow_logic:
|
||||||
|
raise DocForgeError(
|
||||||
|
"adapter_policy_forbids_logic",
|
||||||
|
(
|
||||||
|
"This index preserves a no-AST adapter and refuses function-Logic "
|
||||||
|
"publication"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return projections
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
def check(self, *, verify_rows: bool = True) -> dict[str, object]:
|
def check(self, *, verify_rows: bool = True) -> dict[str, object]:
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,10 @@ class DocForgeService:
|
||||||
context_provider: ContextProvider = compile_context,
|
context_provider: ContextProvider = compile_context,
|
||||||
tool_surface: tuple[str, ...] | None = None,
|
tool_surface: tuple[str, ...] | None = None,
|
||||||
binding_metadata: Mapping[str, object] | None = None,
|
binding_metadata: Mapping[str, object] | None = None,
|
||||||
|
no_ast: bool = False,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.project = project
|
self.project = project
|
||||||
self.index = ProjectIndex(self.project)
|
self.index = ProjectIndex(self.project, allow_logic=not no_ast)
|
||||||
self.changesets = ChangesetStore(self.project, proposal_writer)
|
self.changesets = ChangesetStore(self.project, proposal_writer)
|
||||||
self.rendering = RenderService(self.project, self.changesets)
|
self.rendering = RenderService(self.project, self.changesets)
|
||||||
self.application = CanonicalApplicationService(
|
self.application = CanonicalApplicationService(
|
||||||
|
|
@ -125,11 +126,37 @@ class DocForgeService:
|
||||||
self.visualization = ViewerManagerClient(self.index)
|
self.visualization = ViewerManagerClient(self.index)
|
||||||
self.context_provider = context_provider
|
self.context_provider = context_provider
|
||||||
self.binding_metadata = dict(binding_metadata or {})
|
self.binding_metadata = dict(binding_metadata or {})
|
||||||
|
self.no_ast = no_ast
|
||||||
self.tool_surface = tool_surface or (
|
self.tool_surface = tool_surface or (
|
||||||
*ALL_TOOLS,
|
*ALL_TOOLS,
|
||||||
*(APPLICATION_TOOLS if self.application.enabled else ()),
|
*(APPLICATION_TOOLS if self.application.enabled else ()),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def adapter_policy(self) -> dict[str, object]:
|
||||||
|
"""Return the immutable adapter-evolution policy for this MCP binding."""
|
||||||
|
|
||||||
|
if not self.no_ast:
|
||||||
|
return {
|
||||||
|
"mode": "standard",
|
||||||
|
"ast_analysis": "allowed",
|
||||||
|
"logic_projection": "allowed",
|
||||||
|
"incremental_extraction": "allowed",
|
||||||
|
"adapter_rewrite": "not_requested",
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"mode": "preserve-no-ast",
|
||||||
|
"ast_analysis": "forbidden",
|
||||||
|
"logic_projection": "forbidden",
|
||||||
|
"incremental_extraction": "allowed",
|
||||||
|
"adapter_rewrite": "forbidden",
|
||||||
|
"blocked_tools": ["docforge_get_logic"],
|
||||||
|
"instruction": (
|
||||||
|
"Preserve the existing adapter extraction strategy. Do not add Python AST, "
|
||||||
|
"Tree-sitter, compiler-AST, or function-Logic extraction. Non-AST incremental "
|
||||||
|
"fingerprinting and caching remain allowed."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
def invoke(
|
def invoke(
|
||||||
self,
|
self,
|
||||||
operation: Callable[[], dict[str, object]],
|
operation: Callable[[], dict[str, object]],
|
||||||
|
|
@ -238,7 +265,25 @@ class DocForgeService:
|
||||||
"index_path": str(snapshot.descriptor.index_path),
|
"index_path": str(snapshot.descriptor.index_path),
|
||||||
"changeset_root": str(snapshot.descriptor.changeset_root),
|
"changeset_root": str(snapshot.descriptor.changeset_root),
|
||||||
**self.binding_metadata,
|
**self.binding_metadata,
|
||||||
|
"adapter_policy": self.adapter_policy(),
|
||||||
}
|
}
|
||||||
|
recommended_workflow = [
|
||||||
|
"docforge_get_context or targeted read tools",
|
||||||
|
"make and verify one coherent implementation slice",
|
||||||
|
"docforge_sync",
|
||||||
|
"docforge_register_changes",
|
||||||
|
"docforge_get_changeset_diff",
|
||||||
|
"docforge_apply_changeset",
|
||||||
|
"docforge_bootstrap",
|
||||||
|
]
|
||||||
|
if self.no_ast:
|
||||||
|
recommended_workflow.insert(
|
||||||
|
1,
|
||||||
|
(
|
||||||
|
"preserve the current adapter; do not add AST, Tree-sitter, "
|
||||||
|
"compiler-AST, or function-Logic extraction"
|
||||||
|
),
|
||||||
|
)
|
||||||
return {
|
return {
|
||||||
"status": "ok",
|
"status": "ok",
|
||||||
"project_id": snapshot.descriptor.project_id,
|
"project_id": snapshot.descriptor.project_id,
|
||||||
|
|
@ -249,18 +294,11 @@ class DocForgeService:
|
||||||
"source_hash": snapshot.source_hash,
|
"source_hash": snapshot.source_hash,
|
||||||
"binding": binding,
|
"binding": binding,
|
||||||
"canonical_paths": [str(path) for path in snapshot.descriptor.content_roots],
|
"canonical_paths": [str(path) for path in snapshot.descriptor.content_roots],
|
||||||
|
"adapter_policy": self.adapter_policy(),
|
||||||
"proposal_access": self.changesets.access(),
|
"proposal_access": self.changesets.access(),
|
||||||
"canonical_application_access": self.application.access(),
|
"canonical_application_access": self.application.access(),
|
||||||
"synchronization": synchronized["synchronization"],
|
"synchronization": synchronized["synchronization"],
|
||||||
"recommended_workflow": [
|
"recommended_workflow": recommended_workflow,
|
||||||
"docforge_get_context or targeted read tools",
|
|
||||||
"make and verify one coherent implementation slice",
|
|
||||||
"docforge_sync",
|
|
||||||
"docforge_register_changes",
|
|
||||||
"docforge_get_changeset_diff",
|
|
||||||
"docforge_apply_changeset",
|
|
||||||
"docforge_bootstrap",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return self.invoke(operation, synchronize=False)
|
return self.invoke(operation, synchronize=False)
|
||||||
|
|
@ -310,6 +348,7 @@ class DocForgeService:
|
||||||
"authority_rule": (
|
"authority_rule": (
|
||||||
"Canonical project files own facts; DocForge results are derived."
|
"Canonical project files own facts; DocForge results are derived."
|
||||||
),
|
),
|
||||||
|
"adapter_policy": self.adapter_policy(),
|
||||||
"canonical_paths": [
|
"canonical_paths": [
|
||||||
*(relative(path) for path in snapshot.descriptor.content_roots),
|
*(relative(path) for path in snapshot.descriptor.content_roots),
|
||||||
*(relative(path) for path in snapshot.descriptor.authority_files),
|
*(relative(path) for path in snapshot.descriptor.authority_files),
|
||||||
|
|
@ -349,6 +388,7 @@ class DocForgeService:
|
||||||
else ()
|
else ()
|
||||||
)
|
)
|
||||||
+ (READ_ONLY_EXCLUDED_OPERATIONS if self.tool_surface == READ_TOOLS else ())
|
+ (READ_ONLY_EXCLUDED_OPERATIONS if self.tool_surface == READ_TOOLS else ())
|
||||||
|
+ (("adapter_ast_upgrade", "function_logic_extraction") if self.no_ast else ())
|
||||||
),
|
),
|
||||||
"proposal_access": self.changesets.access(),
|
"proposal_access": self.changesets.access(),
|
||||||
"canonical_application_access": self.application.access(),
|
"canonical_application_access": self.application.access(),
|
||||||
|
|
@ -359,6 +399,23 @@ class DocForgeService:
|
||||||
|
|
||||||
return self.invoke(operation)
|
return self.invoke(operation)
|
||||||
|
|
||||||
|
def get_logic(self, owner_node_id: str) -> dict[str, object]:
|
||||||
|
"""Return one Logic projection unless the binding preserves a no-AST adapter."""
|
||||||
|
|
||||||
|
if self.no_ast:
|
||||||
|
|
||||||
|
def forbidden() -> dict[str, object]:
|
||||||
|
raise DocForgeError(
|
||||||
|
"adapter_policy_forbids_logic",
|
||||||
|
(
|
||||||
|
"This MCP binding preserves a no-AST adapter and forbids function-Logic "
|
||||||
|
"extraction"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.invoke(forbidden, synchronize=False)
|
||||||
|
return self.invoke(lambda: self.index.get_logic(owner_node_id))
|
||||||
|
|
||||||
def validate_project(self) -> dict[str, object]:
|
def validate_project(self) -> dict[str, object]:
|
||||||
def operation() -> dict[str, object]:
|
def operation() -> dict[str, object]:
|
||||||
snapshot = self.project.load()
|
snapshot = self.project.load()
|
||||||
|
|
@ -437,6 +494,13 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
|
||||||
"docforge_register_changes creates a complete proposal atomically. "
|
"docforge_register_changes creates a complete proposal atomically. "
|
||||||
"This server exposes no arbitrary renderer, shell, Git, deployment, publication, "
|
"This server exposes no arbitrary renderer, shell, Git, deployment, publication, "
|
||||||
"or project switching."
|
"or project switching."
|
||||||
|
+ (
|
||||||
|
" This binding preserves the existing adapter and forbids AST, Tree-sitter, "
|
||||||
|
"compiler-AST, and function-Logic extraction changes. Do not rewrite or upgrade "
|
||||||
|
"the adapter to add those capabilities."
|
||||||
|
if service.no_ast
|
||||||
|
else ""
|
||||||
|
)
|
||||||
),
|
),
|
||||||
json_response=True,
|
json_response=True,
|
||||||
)
|
)
|
||||||
|
|
@ -475,7 +539,7 @@ def _create_bound_server(service: DocForgeService, *, read_only: bool) -> FastMC
|
||||||
def get_logic(owner_node_id: str) -> dict[str, Any]:
|
def get_logic(owner_node_id: str) -> dict[str, Any]:
|
||||||
"""Return the lazy control-flow projection owned by one function or method."""
|
"""Return the lazy control-flow projection owned by one function or method."""
|
||||||
|
|
||||||
return service.invoke(lambda: service.index.get_logic(owner_node_id))
|
return service.get_logic(owner_node_id)
|
||||||
|
|
||||||
@server.tool(name="docforge_search")
|
@server.tool(name="docforge_search")
|
||||||
def search(query: str, limit: int | None = None) -> dict[str, Any]:
|
def search(query: str, limit: int | None = None) -> dict[str, Any]:
|
||||||
|
|
@ -822,6 +886,7 @@ def create_server(
|
||||||
proposal_writer: str | None = None,
|
proposal_writer: str | None = None,
|
||||||
*,
|
*,
|
||||||
canonical_applier_id: str | None = None,
|
canonical_applier_id: str | None = None,
|
||||||
|
no_ast: bool = False,
|
||||||
) -> FastMCP:
|
) -> FastMCP:
|
||||||
project = Project.open(project_root)
|
project = Project.open(project_root)
|
||||||
return create_project_server(
|
return create_project_server(
|
||||||
|
|
@ -835,6 +900,7 @@ def create_server(
|
||||||
"server_module": "docforge.mcp_server",
|
"server_module": "docforge.mcp_server",
|
||||||
"adapter_mode": "generic",
|
"adapter_mode": "generic",
|
||||||
},
|
},
|
||||||
|
no_ast=no_ast,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -846,6 +912,7 @@ def create_project_server(
|
||||||
canonical_applier: CanonicalApplier | None = None,
|
canonical_applier: CanonicalApplier | None = None,
|
||||||
context_provider: ContextProvider = compile_context,
|
context_provider: ContextProvider = compile_context,
|
||||||
binding_metadata: Mapping[str, object] | None = None,
|
binding_metadata: Mapping[str, object] | None = None,
|
||||||
|
no_ast: bool = False,
|
||||||
) -> FastMCP:
|
) -> FastMCP:
|
||||||
"""Create the full fixed MCP surface for one explicitly configured project service."""
|
"""Create the full fixed MCP surface for one explicitly configured project service."""
|
||||||
|
|
||||||
|
|
@ -856,6 +923,7 @@ def create_project_server(
|
||||||
canonical_applier=canonical_applier,
|
canonical_applier=canonical_applier,
|
||||||
context_provider=context_provider,
|
context_provider=context_provider,
|
||||||
binding_metadata=binding_metadata,
|
binding_metadata=binding_metadata,
|
||||||
|
no_ast=no_ast,
|
||||||
)
|
)
|
||||||
return _create_bound_server(service, read_only=False)
|
return _create_bound_server(service, read_only=False)
|
||||||
|
|
||||||
|
|
@ -865,6 +933,7 @@ def create_read_only_server(
|
||||||
*,
|
*,
|
||||||
context_provider: ContextProvider = compile_context,
|
context_provider: ContextProvider = compile_context,
|
||||||
binding_metadata: Mapping[str, object] | None = None,
|
binding_metadata: Mapping[str, object] | None = None,
|
||||||
|
no_ast: bool = False,
|
||||||
) -> FastMCP:
|
) -> FastMCP:
|
||||||
"""Create an adapter-capable MCP server exposing only the fixed read tool surface."""
|
"""Create an adapter-capable MCP server exposing only the fixed read tool surface."""
|
||||||
|
|
||||||
|
|
@ -873,6 +942,7 @@ def create_read_only_server(
|
||||||
context_provider=context_provider,
|
context_provider=context_provider,
|
||||||
tool_surface=READ_TOOLS,
|
tool_surface=READ_TOOLS,
|
||||||
binding_metadata=binding_metadata,
|
binding_metadata=binding_metadata,
|
||||||
|
no_ast=no_ast,
|
||||||
)
|
)
|
||||||
return _create_bound_server(service, read_only=True)
|
return _create_bound_server(service, read_only=True)
|
||||||
|
|
||||||
|
|
@ -882,11 +952,20 @@ def main() -> None:
|
||||||
parser.add_argument("--project-root", type=Path, required=True)
|
parser.add_argument("--project-root", type=Path, required=True)
|
||||||
parser.add_argument("--proposal-writer")
|
parser.add_argument("--proposal-writer")
|
||||||
parser.add_argument("--canonical-applier")
|
parser.add_argument("--canonical-applier")
|
||||||
|
parser.add_argument(
|
||||||
|
"--no-ast",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Preserve the existing adapter and forbid AST, Tree-sitter, compiler-AST, "
|
||||||
|
"and function-Logic extraction changes"
|
||||||
|
),
|
||||||
|
)
|
||||||
arguments = parser.parse_args()
|
arguments = parser.parse_args()
|
||||||
create_server(
|
create_server(
|
||||||
arguments.project_root,
|
arguments.project_root,
|
||||||
arguments.proposal_writer,
|
arguments.proposal_writer,
|
||||||
canonical_applier_id=arguments.canonical_applier,
|
canonical_applier_id=arguments.canonical_applier,
|
||||||
|
no_ast=arguments.no_ast,
|
||||||
).run(transport="stdio")
|
).run(transport="stdio")
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -366,6 +366,7 @@ class AdapterContractTests(unittest.TestCase):
|
||||||
self.assertTrue(visual_logic["available"])
|
self.assertTrue(visual_logic["available"])
|
||||||
self.assertEqual("entry", visual_logic["root"])
|
self.assertEqual("entry", visual_logic["root"])
|
||||||
self.assertEqual("return", visual_logic["edges"][0]["relation"])
|
self.assertEqual("return", visual_logic["edges"][0]["relation"])
|
||||||
|
|
||||||
loader.extract_calls.clear()
|
loader.extract_calls.clear()
|
||||||
cache_path = root / ".cache" / "incremental" / "extractions.json"
|
cache_path = root / ".cache" / "incremental" / "extractions.json"
|
||||||
cache_modified = cache_path.stat().st_mtime_ns
|
cache_modified = cache_path.stat().st_mtime_ns
|
||||||
|
|
@ -403,6 +404,20 @@ class AdapterContractTests(unittest.TestCase):
|
||||||
index.get_node("guide.workflow")["node"]["source_path"],
|
index.get_node("guide.workflow")["node"]["source_path"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_no_ast_index_policy_rejects_logic_publication(self) -> None:
|
||||||
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
root = Path(directory).resolve()
|
||||||
|
project = AdapterProject(
|
||||||
|
IncrementalLoader(root),
|
||||||
|
cache_root=root / ".cache" / "no-ast",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(DocForgeError) as captured:
|
||||||
|
ProjectIndex(project, allow_logic=False).build()
|
||||||
|
|
||||||
|
self.assertEqual("adapter_policy_forbids_logic", captured.exception.code)
|
||||||
|
self.assertFalse(project.descriptor.index_path.exists())
|
||||||
|
|
||||||
def test_fast_incremental_reads_reverify_a_changed_index_file(self) -> None:
|
def test_fast_incremental_reads_reverify_a_changed_index_file(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = Path(directory).resolve()
|
root = Path(directory).resolve()
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,44 @@ class DocForgeMcpTests(unittest.IsolatedAsyncioTestCase):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def test_no_ast_binding_preserves_adapter_and_blocks_logic(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, no_ast=True), raise_exceptions=True
|
||||||
|
) as session:
|
||||||
|
bootstrap = await session.call_tool("docforge_bootstrap", {})
|
||||||
|
contract = await session.call_tool("docforge_get_contract", {})
|
||||||
|
logic = await session.call_tool(
|
||||||
|
"docforge_get_logic", {"owner_node_id": "guide.workflow"}
|
||||||
|
)
|
||||||
|
|
||||||
|
policy = bootstrap.structuredContent["adapter_policy"]
|
||||||
|
self.assertEqual("preserve-no-ast", policy["mode"])
|
||||||
|
self.assertEqual("forbidden", policy["ast_analysis"])
|
||||||
|
self.assertEqual("forbidden", policy["logic_projection"])
|
||||||
|
self.assertEqual("allowed", policy["incremental_extraction"])
|
||||||
|
self.assertEqual(["docforge_get_logic"], policy["blocked_tools"])
|
||||||
|
self.assertEqual(
|
||||||
|
policy,
|
||||||
|
bootstrap.structuredContent["binding"]["adapter_policy"],
|
||||||
|
)
|
||||||
|
self.assertIn(
|
||||||
|
"preserve the current adapter",
|
||||||
|
bootstrap.structuredContent["recommended_workflow"][1],
|
||||||
|
)
|
||||||
|
self.assertEqual(policy, contract.structuredContent["adapter_policy"])
|
||||||
|
self.assertIn(
|
||||||
|
"adapter_ast_upgrade",
|
||||||
|
contract.structuredContent["excluded_operations"],
|
||||||
|
)
|
||||||
|
self.assertEqual("error", logic.structuredContent["status"])
|
||||||
|
self.assertEqual(
|
||||||
|
"adapter_policy_forbids_logic",
|
||||||
|
logic.structuredContent["error"]["code"],
|
||||||
|
)
|
||||||
|
|
||||||
async def test_every_read_tool_returns_scoped_structured_results(self) -> None:
|
async def test_every_read_tool_returns_scoped_structured_results(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
root = self.copy_fixture("alpha", Path(directory))
|
root = self.copy_fixture("alpha", Path(directory))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue