Extension framework optimization

This commit is contained in:
2026-05-04 11:49:39 +02:00
parent 32f4af8359
commit 33fa602fe5
7 changed files with 208 additions and 18 deletions

View File

@@ -94,6 +94,58 @@ Subsystem-specific dataclasses may remain richer. The canonical model is the
bridge that lets callbacks, registries, diagnostics, provenance, and future
policy checks interact consistently.
### Minimal Runnable Extension
```python
from markitect_tool.extension import (
ExtensionDescriptor,
ExtensionExecutor,
ExtensionRegistry,
ProcessingRequest,
ProcessingResult,
)
def run_example(request: ProcessingRequest) -> ProcessingResult:
name = request.input.get("name", "world")
return ProcessingResult(output=f"Hello, {name}")
descriptor = ExtensionDescriptor(
id="example.hello",
kind="example",
summary="Small example extension.",
factory=lambda: run_example,
)
registry = ExtensionRegistry([descriptor])
result = ExtensionExecutor(registry).execute(
"example.hello",
ProcessingRequest(operation="example.hello", input={"name": "Markitect"}),
)
```
Use this executor boundary when callbacks, dependency checks, trace events, or
future policy checks matter. For tiny deterministic helpers, it is still fine to
keep the existing direct function API and expose a descriptor alongside it.
### Cache-Key Rules
`ProcessingRequest.cache_key` includes:
- operation
- input
- stable context material
- options
- scope
- declared capabilities
- request metadata
Stable context material includes source path, namespaces, variables, policy, and
metadata. It does not include workspace root, caller, or live backend handles.
This keeps cache keys portable while avoiding collisions for context-sensitive
operations.
## Diagnostics
Diagnostics should be:

View File

@@ -95,6 +95,63 @@ The canonical processing model should define a small set of shared envelopes:
Subsystem-specific types may remain richer. The canonical model is the bridge,
not a forced replacement for every local dataclass.
### Processing Request Example
```python
from pathlib import Path
from markitect_tool.extension import ProcessingContext, ProcessingRequest
request = ProcessingRequest(
operation="query.selector",
input={"selector": "sections[heading=Decision]"},
context=ProcessingContext(
source_path=Path("docs/adr.md"),
namespaces={"std": "standards"},
variables={"audience": "internal"},
),
options={"format": "json"},
scope="document",
)
```
The request cache key includes operation, input, options, scope, declared
capabilities, metadata, and stable context semantics:
- source path
- namespaces
- variables
- policy
- metadata
It intentionally excludes root path, caller name, and live backend handles.
Those are execution-environment details. If they matter semantically for an
extension, put explicit values in request options or metadata.
### Processing Result Example
```python
from markitect_tool.extension import (
ProcessingProvenance,
ProcessingResult,
ProcessingTrace,
)
result = ProcessingResult(
output={"count": 1},
provenance=[
ProcessingProvenance(
operation="query.selector",
source_path="docs/adr.md",
content_hash="sha256:...",
)
],
).with_trace(ProcessingTrace(event="query.done"))
```
`ProcessingResult.valid` is derived from diagnostics. Any diagnostic with
severity `error` makes the result invalid.
## Registration Strategy
Start with in-package registration:
@@ -115,6 +172,35 @@ packages become a real requirement.
See `docs/extension-authoring.md` for the extension authoring checklist and
descriptor template.
### Registry Use
Extension registries are optimized for common lookup patterns:
- `registry.get("backend.local-sqlite")`
- `registry.list(kind="query-engine")`
- `registry.require_capability("fts")`
- `registry.check_dependencies("jsonpath")`
Kinds and capabilities are indexed at registration time, so large registries can
avoid repeated full scans for basic discovery.
### Execution Lifecycle
`ExtensionExecutor` wraps a descriptor factory with deterministic lifecycle
hooks:
1. Fetch descriptor.
2. Check required optional dependencies.
3. Instantiate callable implementation.
4. Run `before` callbacks.
5. Execute implementation.
6. Normalize result type.
7. Append `extension.executed` trace.
8. Run success or failure callbacks.
9. Run final `after` callbacks.
Callbacks are explicit. The framework does not introduce hidden global behavior.
## Compatibility Rules
The refactor must preserve: