generated from coulomb/repo-seed
Add render export adapter contract
This commit is contained in:
@@ -13,6 +13,9 @@ Generated from `markitect_tool.__all__`.
|
||||
- `LOCAL_INDEX_SCHEMA_VERSION` - object. str(object='') -> str
|
||||
- `MAX_FUNCTION_PIPELINE_DEPTH` - object. int([x]) -> integer
|
||||
- `NORMALIZED_SOURCE_SCHEMA_VERSION` - object. str(object='') -> str
|
||||
- `RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str
|
||||
- `RENDER_EXPORT_ADAPTER_KIND` - object. str(object='') -> str
|
||||
- `RENDER_EXPORT_SCHEMA_VERSION` - object. str(object='') -> str
|
||||
- `SOURCE_ADAPTER_ENTRY_POINT_GROUP` - object. str(object='') -> str
|
||||
|
||||
## `markitect_tool.backend.engine`
|
||||
@@ -310,6 +313,23 @@ Generated from `markitect_tool.__all__`.
|
||||
- `parse_reference(reference: 'str') -> 'ReferenceAddress'` - function. Parse a compact Markitect content reference.
|
||||
- `resolve_reference(reference: 'str | ReferenceAddress', *, context: 'ReferenceContext') -> 'ReferenceResolution'` - function. Resolve a content reference to one or more content units.
|
||||
|
||||
## `markitect_tool.render.engine`
|
||||
|
||||
- `FakeRenderExportAdapter() -> 'None'` - class. Deterministic no-op renderer used for contract tests.
|
||||
- `RenderArtifact(artifact_id: 'str', role: 'str', media_type: 'str', content: 'str | None' = None, path: 'str | None' = None, uri: 'str | None' = None, digest: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Metadata for a rendered or exported artifact.
|
||||
- `RenderExportAdapter(*args, **kwargs)` - class. Render/export adapter protocol.
|
||||
- `RenderExportAdapterDescriptor(id: 'str', version: 'str', name: 'str', operations: 'list[str]', input_contracts: 'list[str]', output_profiles: 'list[str]', artifact_media_types: 'list[str]', factory: 'RenderExportAdapterFactory', summary: 'str | None' = None, option_schema: 'dict[str, Any]' = <factory>, optional_dependencies: 'list[OptionalDependency]' = <factory>, safety: 'dict[str, Any]' = <factory>, quality_profile: 'dict[str, Any]' = <factory>, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Inspectable descriptor for one render/export adapter.
|
||||
- `RenderExportAdapterError` - class. Raised when render/export adapter contracts are invalid.
|
||||
- `RenderExportAdapterRegistry(descriptors: 'Iterable[RenderExportAdapterDescriptor] | None' = None) -> 'None'` - class. Registry of render/export adapter descriptors.
|
||||
- `RenderExportRequest(source: 'str', operation: 'str' = 'render-artifact', profile: 'str' = 'plain', source_path: 'str | None' = None, options: 'dict[str, Any]' = <factory>, policy: 'dict[str, Any]' = <factory>, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Service-free render/export request.
|
||||
- `RenderExportResult(adapter: 'dict[str, Any]', operation: 'str', profile: 'str', artifacts: 'list[RenderArtifact]' = <factory>, exported_source: 'str | None' = None, diagnostics: 'list[Diagnostic]' = <factory>, provenance: 'list[RenderProvenance]' = <factory>, schema_version: 'str' = 'markitect.render.export.v1', metadata: 'dict[str, Any]' = <factory>) -> None` - class. Result of a render/export adapter operation.
|
||||
- `RenderProvenance(operation: 'str', adapter_id: 'str', profile: 'str', source_path: 'str | None' = None, source_digest: 'str | None' = None, artifact_id: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. Source-to-render provenance envelope.
|
||||
- `default_render_export_adapter_registry() -> 'RenderExportAdapterRegistry'` - function. Return the built-in render/export adapter registry.
|
||||
- `discover_render_export_adapters() -> 'list[RenderExportAdapterDescriptor]'` - function. Discover package-provided render/export adapter descriptors.
|
||||
- `render_capability_diagnostics(descriptor: 'RenderExportAdapterDescriptor', request: 'RenderExportRequest') -> 'list[Diagnostic]'` - function. Return diagnostics for capabilities blocked by request policy.
|
||||
- `render_export_registry_descriptor() -> 'ExtensionDescriptor'` - function. Descriptor for the render/export adapter registry itself.
|
||||
- `render_with_adapter(request: 'RenderExportRequest', *, registry: 'RenderExportAdapterRegistry | None' = None, adapter_id: 'str' = 'render.fake') -> 'RenderExportResult'` - function. Render/export through a registered adapter.
|
||||
|
||||
## `markitect_tool.runtime.assessment`
|
||||
|
||||
- `AssessmentRequest(contract_id: 'str | None', rule_id: 'str', scope: 'str', text: 'str', criteria: 'Any', context: 'dict[str, Any]' = <factory>, structured_inputs: 'dict[str, Any]' = <factory>, severity: 'str' = 'error', threshold: 'float | None' = None, provider: 'str | None' = None, model: 'str | None' = None, metadata: 'dict[str, Any]' = <factory>) -> None` - class. A provider-neutral request for rubric assessment.
|
||||
|
||||
@@ -37,6 +37,12 @@ This index maps example files to practical usecases and useful commands.
|
||||
| `examples/source-adapters/*.json`, `examples/source-adapters/normalized-output.md` | Expected envelopes for the v1 source adapter contract | Use as fixtures for `mkt source` commands after `MKTT-WP-0018` implementation |
|
||||
| `examples/source-adapters/fake-adapter-pyproject.toml` | External adapter entry point shape | Use as the fake package discovery fixture for source adapter contract tests |
|
||||
|
||||
## Render Export Contract Fixtures
|
||||
|
||||
| Files | Usecase | Try |
|
||||
| --- | --- | --- |
|
||||
| `examples/render/fake-render-request.yaml` | Deterministic render/export contract fixture | Use with the `render.fake` API adapter; no external renderer required |
|
||||
|
||||
## Cache, Backend, Policy, And Context
|
||||
|
||||
| Files | Usecase | Try |
|
||||
|
||||
@@ -41,7 +41,7 @@ framework organizes how Markitect itself exposes and composes capabilities.
|
||||
| `generation-adapter` | provider-neutral assisted generation | request in, generated candidate out |
|
||||
| `source-adapter` | EPUB3/PDF/DOCX adapters in external packages | source asset in, normalized Markdown out |
|
||||
| `cli-group` | cache, backend, ref, class | command descriptors or registration hook |
|
||||
| `render-export` | future Quarkdown/export adapters | Markdown source in, rendered/exported artifact out |
|
||||
| `render-export` | Quarkdown/export adapters | Markdown source in, rendered/exported artifact descriptor out |
|
||||
| `document-function` | future function layer | function call in, typed document value out |
|
||||
|
||||
## Canonical Lifecycle
|
||||
|
||||
87
docs/render-export-adapters.md
Normal file
87
docs/render-export-adapters.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# Render Export Adapter Contract
|
||||
|
||||
Markitect owns the contract layer for optional render/export adapters. It does
|
||||
not run real renderers in core, install renderer dependencies, store durable
|
||||
artifacts, or publish outputs.
|
||||
|
||||
This is the output-side sibling of the read-only source adapter contract:
|
||||
|
||||
```text
|
||||
source adapters: source formats -> normalized Markdown
|
||||
render adapters: Markdown/context -> renderer source or artifact metadata
|
||||
```
|
||||
|
||||
`markitect-filter` remains read-side only. Concrete Quarkdown execution belongs
|
||||
in `markitect-quarkdown`.
|
||||
|
||||
## Contract Version
|
||||
|
||||
- `markitect.render.export.v1`
|
||||
|
||||
The optional package entry point group is:
|
||||
|
||||
```text
|
||||
markitect_tool.render_export_adapters
|
||||
```
|
||||
|
||||
## Descriptor Shape
|
||||
|
||||
Render/export adapters declare:
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `id` | Stable adapter id, for example `render.fake` or `render.quarkdown`. |
|
||||
| `version` | Adapter contract implementation version. |
|
||||
| `operations` | Supported operations: `inspect-profile`, `export-source`, `render-artifact`. |
|
||||
| `input_contracts` | Accepted inputs, such as Markdown or function evaluation results. |
|
||||
| `output_profiles` | Supported profiles: `plain`, `docs`, `slides`, `paged`, `static-site`, `pdf`. |
|
||||
| `artifact_media_types` | Artifact media types the adapter may emit. |
|
||||
| `safety` | Declared filesystem, process, network, native dependency, and side-effect behavior. |
|
||||
|
||||
The built-in `render.fake` adapter is deterministic and never invokes external
|
||||
processes. It exists to test the contract and examples.
|
||||
|
||||
## Request And Result
|
||||
|
||||
`RenderExportRequest` contains Markdown source, operation, profile, source
|
||||
identity, options, and local policy flags.
|
||||
|
||||
`RenderExportResult` contains:
|
||||
|
||||
- adapter identity
|
||||
- operation and profile
|
||||
- exported renderer source where applicable
|
||||
- artifact metadata
|
||||
- diagnostics
|
||||
- source-to-render provenance
|
||||
- metadata such as whether an external renderer was invoked
|
||||
|
||||
Artifacts are descriptors, not durable storage records. Real renderer packages
|
||||
may write files, but core Markitect only models the result.
|
||||
|
||||
## Capability Gates
|
||||
|
||||
Adapters declare local safety flags:
|
||||
|
||||
- `filesystem_read`
|
||||
- `filesystem_write`
|
||||
- `external_process`
|
||||
- `network`
|
||||
- `native_renderer_dependency`
|
||||
- `assisted_generation`
|
||||
- `publication_side_effect`
|
||||
|
||||
`render_capability_diagnostics` maps those flags to denied-operation
|
||||
diagnostics when a request policy blocks them. This is a contract boundary, not
|
||||
a durable authorization service.
|
||||
|
||||
## Quarkdown Handoff
|
||||
|
||||
`MQD-WP-0001` should implement a concrete `render.quarkdown` adapter in
|
||||
`markitect-quarkdown` against this contract. That adapter should own Quarkdown
|
||||
CLI invocation, Java/Node/Puppeteer assumptions, Quarkdown permissions, output
|
||||
directory conventions, artifact validation, and upstream compatibility
|
||||
monitoring.
|
||||
|
||||
`markitect-tool` keeps only the render/export schema, descriptor registry,
|
||||
fake renderer, diagnostics, provenance, and tests.
|
||||
@@ -44,8 +44,10 @@ and descriptions mirror the operational view.
|
||||
| `MKTT-WP-0017` | complete | done | `MKTT-WP-0003`, `MKTT-WP-0013` | CLI/API polish and practical adoption track is complete: shell completion, extension discovery, generated CLI/API docs, usecase relevance matrix, E2E fixture matrix, large-corpus smoke coverage, first-use docs, examples index, and command cheat sheet. |
|
||||
| `MKTT-WP-0019` | complete | done | `MKTT-WP-0013`, `MKTT-WP-0017` | Source adapter contract refinement is complete: v1 read-only scope, normalized model fields, package entry point discovery, CLI/API envelopes, fake adapter fixtures, and `markitect-filter` EPUB3 handoff are pinned in `docs/source-adapter-contract.md`. |
|
||||
| `MKTT-WP-0018` | complete | done | `MKTT-WP-0013`, `MKTT-WP-0017`, `MKTT-WP-0019` | Source adapter framework implementation is complete: read-only models, protocol, registry, entry point discovery, extension descriptors, CLI/API, fake adapter fixtures, migration notes, and tests are in place. |
|
||||
| `MKTT-WP-0015` | P2 | todo | `MKTT-WP-0010`, `MKTT-WP-0011`, `MKTT-WP-0012` | Future render and document-function extensions: typed values, richer syntax, document-local reusable functions, Quarkdown/export adapters, render-aware references, assets, and permission sandboxing. Defer unless publishing/export pressure becomes current. |
|
||||
| `MKTT-WP-0016` | P2 | todo | `MKTT-WP-0008`, `MKTT-WP-0007`, `MKTT-WP-0009`, `MKTT-WP-0013` | Follow-on agentic memory architecture: reasoning decision graphs, conversational paths, long-term knowledge graphs, memory service blueprints/profiles, graph-to-context-package compilation, and adapter boundaries. |
|
||||
| `MKTT-WP-0015` | complete | done | `MKTT-WP-0010`, `MKTT-WP-0011`, `MKTT-WP-0012` | Document function value contracts are complete: typed values, deterministic Markdown/JSON mapping, descriptor output validation, API exports, docs, examples, and tests. |
|
||||
| `MKTT-WP-0016` | complete | done | `MKTT-WP-0008`, `MKTT-WP-0007`, `MKTT-WP-0009`, `MKTT-WP-0013` | Memory graph profile contracts are complete: graph/profile/event models, validation, context-package compilation, CLI, fixture breadth, invalid fixtures, and runtime adapter handoff descriptors. |
|
||||
| `MKTT-WP-0020` | complete | done | `MKTT-WP-0013`, `MKTT-WP-0015` | Render/export adapter contract is complete: descriptors, registry/discovery, request/result/artifact/provenance envelopes, fake deterministic renderer, capability diagnostics, extension descriptors, docs, examples, and tests. |
|
||||
| `MKTT-WP-0021` | P2 | todo | `MKTT-WP-0010`, `MKTT-WP-0015`, `MKTT-WP-0020` | Render reference and asset manifest contract remains open: passive references, numbered unit metadata, static asset manifests, and source-to-render provenance maps. |
|
||||
|
||||
## Dependency Notes
|
||||
|
||||
|
||||
16
examples/render/fake-render-request.yaml
Normal file
16
examples/render/fake-render-request.yaml
Normal file
@@ -0,0 +1,16 @@
|
||||
schema_version: markitect.render.export.v1
|
||||
operation: render-artifact
|
||||
profile: docs
|
||||
source_path: examples/render/source.md
|
||||
source: |
|
||||
# Fake Render Example
|
||||
|
||||
This fixture exercises the render/export adapter contract without invoking a
|
||||
real renderer.
|
||||
options:
|
||||
deterministic: true
|
||||
policy:
|
||||
external_process: false
|
||||
filesystem_write: false
|
||||
metadata:
|
||||
purpose: render-contract-fixture
|
||||
@@ -259,6 +259,25 @@ from markitect_tool.runtime import (
|
||||
load_runtime_context_file_result,
|
||||
run_contract_assessments,
|
||||
)
|
||||
from markitect_tool.render import (
|
||||
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
|
||||
RENDER_EXPORT_ADAPTER_KIND,
|
||||
RENDER_EXPORT_SCHEMA_VERSION,
|
||||
FakeRenderExportAdapter,
|
||||
RenderArtifact,
|
||||
RenderExportAdapter,
|
||||
RenderExportAdapterDescriptor,
|
||||
RenderExportAdapterError,
|
||||
RenderExportAdapterRegistry,
|
||||
RenderExportRequest,
|
||||
RenderExportResult,
|
||||
RenderProvenance,
|
||||
default_render_export_adapter_registry,
|
||||
discover_render_export_adapters,
|
||||
render_capability_diagnostics,
|
||||
render_export_registry_descriptor,
|
||||
render_with_adapter,
|
||||
)
|
||||
from markitect_tool.schema import (
|
||||
MarkdownSchema,
|
||||
SchemaValidationResult,
|
||||
@@ -570,6 +589,23 @@ __all__ = [
|
||||
"load_runtime_context_file",
|
||||
"load_runtime_context_file_result",
|
||||
"run_contract_assessments",
|
||||
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
|
||||
"RENDER_EXPORT_ADAPTER_KIND",
|
||||
"RENDER_EXPORT_SCHEMA_VERSION",
|
||||
"FakeRenderExportAdapter",
|
||||
"RenderArtifact",
|
||||
"RenderExportAdapter",
|
||||
"RenderExportAdapterDescriptor",
|
||||
"RenderExportAdapterError",
|
||||
"RenderExportAdapterRegistry",
|
||||
"RenderExportRequest",
|
||||
"RenderExportResult",
|
||||
"RenderProvenance",
|
||||
"default_render_export_adapter_registry",
|
||||
"discover_render_export_adapters",
|
||||
"render_capability_diagnostics",
|
||||
"render_export_registry_descriptor",
|
||||
"render_with_adapter",
|
||||
"MissingTemplateVariable",
|
||||
"TemplateAnalysis",
|
||||
"TemplateError",
|
||||
|
||||
@@ -5,6 +5,10 @@ from __future__ import annotations
|
||||
from markitect_tool.extension.registry import ExtensionDescriptor, ExtensionRegistry
|
||||
from markitect_tool.extension.processing import ProcessingCapability
|
||||
from markitect_tool.query import default_query_engine_registry
|
||||
from markitect_tool.render import (
|
||||
default_render_export_adapter_registry,
|
||||
render_export_registry_descriptor,
|
||||
)
|
||||
from markitect_tool.source import (
|
||||
default_source_adapter_registry,
|
||||
source_adapter_registry_descriptor,
|
||||
@@ -28,9 +32,12 @@ def builtin_extension_registry() -> ExtensionRegistry:
|
||||
_memory_graph_contract_descriptor(),
|
||||
_memory_runtime_adapter_descriptor(),
|
||||
_agent_memory_descriptor(),
|
||||
render_export_registry_descriptor(),
|
||||
source_adapter_registry_descriptor(),
|
||||
]:
|
||||
registry.register(descriptor)
|
||||
for descriptor in default_render_export_adapter_registry().extension_descriptors():
|
||||
registry.register(descriptor)
|
||||
for descriptor in default_source_adapter_registry().extension_descriptors():
|
||||
registry.register(descriptor)
|
||||
return registry
|
||||
|
||||
41
src/markitect_tool/render/__init__.py
Normal file
41
src/markitect_tool/render/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
"""Render/export adapter contracts."""
|
||||
|
||||
from markitect_tool.render.engine import (
|
||||
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
|
||||
RENDER_EXPORT_ADAPTER_KIND,
|
||||
RENDER_EXPORT_SCHEMA_VERSION,
|
||||
FakeRenderExportAdapter,
|
||||
RenderArtifact,
|
||||
RenderExportAdapter,
|
||||
RenderExportAdapterDescriptor,
|
||||
RenderExportAdapterError,
|
||||
RenderExportAdapterRegistry,
|
||||
RenderExportRequest,
|
||||
RenderExportResult,
|
||||
RenderProvenance,
|
||||
default_render_export_adapter_registry,
|
||||
discover_render_export_adapters,
|
||||
render_capability_diagnostics,
|
||||
render_export_registry_descriptor,
|
||||
render_with_adapter,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
|
||||
"RENDER_EXPORT_ADAPTER_KIND",
|
||||
"RENDER_EXPORT_SCHEMA_VERSION",
|
||||
"FakeRenderExportAdapter",
|
||||
"RenderArtifact",
|
||||
"RenderExportAdapter",
|
||||
"RenderExportAdapterDescriptor",
|
||||
"RenderExportAdapterError",
|
||||
"RenderExportAdapterRegistry",
|
||||
"RenderExportRequest",
|
||||
"RenderExportResult",
|
||||
"RenderProvenance",
|
||||
"default_render_export_adapter_registry",
|
||||
"discover_render_export_adapters",
|
||||
"render_capability_diagnostics",
|
||||
"render_export_registry_descriptor",
|
||||
"render_with_adapter",
|
||||
]
|
||||
725
src/markitect_tool/render/engine.py
Normal file
725
src/markitect_tool/render/engine.py
Normal file
@@ -0,0 +1,725 @@
|
||||
"""Contract-only render/export adapter framework."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import asdict, dataclass, field
|
||||
import hashlib
|
||||
import importlib.metadata
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable, Protocol, runtime_checkable
|
||||
|
||||
from markitect_tool.diagnostics import Diagnostic, SourceLocation, has_error
|
||||
from markitect_tool.extension import (
|
||||
ExtensionDependencyCheck,
|
||||
ExtensionDescriptor,
|
||||
OptionalDependency,
|
||||
ProcessingCapability,
|
||||
)
|
||||
|
||||
|
||||
RENDER_EXPORT_SCHEMA_VERSION = "markitect.render.export.v1"
|
||||
RENDER_EXPORT_ADAPTER_KIND = "render-export"
|
||||
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP = "markitect_tool.render_export_adapters"
|
||||
|
||||
RENDER_OPERATIONS = {
|
||||
"inspect-profile",
|
||||
"export-source",
|
||||
"render-artifact",
|
||||
}
|
||||
|
||||
RENDER_PROFILES = {
|
||||
"plain",
|
||||
"docs",
|
||||
"slides",
|
||||
"paged",
|
||||
"static-site",
|
||||
"pdf",
|
||||
}
|
||||
|
||||
_SAFETY_POLICY_FLAGS = {
|
||||
"filesystem_read": "filesystem_read",
|
||||
"filesystem_write": "filesystem_write",
|
||||
"external_process": "external_process",
|
||||
"network": "network",
|
||||
"native_renderer_dependency": "native_renderer_dependency",
|
||||
"assisted_generation": "assisted_generation",
|
||||
"publication_side_effect": "publication_side_effect",
|
||||
}
|
||||
|
||||
|
||||
class RenderExportAdapterError(ValueError):
|
||||
"""Raised when render/export adapter contracts are invalid."""
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderArtifact:
|
||||
"""Metadata for a rendered or exported artifact."""
|
||||
|
||||
artifact_id: str
|
||||
role: str
|
||||
media_type: str
|
||||
content: str | None = None
|
||||
path: str | None = None
|
||||
uri: str | None = None
|
||||
digest: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@classmethod
|
||||
def from_content(
|
||||
cls,
|
||||
content: str,
|
||||
*,
|
||||
role: str,
|
||||
media_type: str,
|
||||
artifact_id: str | None = None,
|
||||
metadata: dict[str, Any] | None = None,
|
||||
) -> "RenderArtifact":
|
||||
digest = _digest_text(content)
|
||||
return cls(
|
||||
artifact_id=artifact_id or f"artifact:{digest.removeprefix('sha256:')[:16]}",
|
||||
role=role,
|
||||
media_type=media_type,
|
||||
content=content,
|
||||
digest=digest,
|
||||
metadata=metadata or {},
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return _drop_empty(asdict(self))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderProvenance:
|
||||
"""Source-to-render provenance envelope."""
|
||||
|
||||
operation: str
|
||||
adapter_id: str
|
||||
profile: str
|
||||
source_path: str | None = None
|
||||
source_digest: str | None = None
|
||||
artifact_id: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return _drop_empty(asdict(self))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderExportRequest:
|
||||
"""Service-free render/export request."""
|
||||
|
||||
source: str
|
||||
operation: str = "render-artifact"
|
||||
profile: str = "plain"
|
||||
source_path: str | None = None
|
||||
options: dict[str, Any] = field(default_factory=dict)
|
||||
policy: dict[str, Any] = field(default_factory=dict)
|
||||
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return _drop_empty(
|
||||
{
|
||||
"schema_version": self.schema_version,
|
||||
"operation": self.operation,
|
||||
"profile": self.profile,
|
||||
"source": self.source,
|
||||
"source_path": self.source_path,
|
||||
"options": self.options,
|
||||
"policy": self.policy,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderExportResult:
|
||||
"""Result of a render/export adapter operation."""
|
||||
|
||||
adapter: dict[str, Any]
|
||||
operation: str
|
||||
profile: str
|
||||
artifacts: list[RenderArtifact] = field(default_factory=list)
|
||||
exported_source: str | None = None
|
||||
diagnostics: list[Diagnostic] = field(default_factory=list)
|
||||
provenance: list[RenderProvenance] = field(default_factory=list)
|
||||
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def valid(self) -> bool:
|
||||
return not has_error(self.diagnostics)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return _drop_empty(
|
||||
{
|
||||
"schema_version": self.schema_version,
|
||||
"valid": self.valid,
|
||||
"adapter": self.adapter,
|
||||
"operation": self.operation,
|
||||
"profile": self.profile,
|
||||
"artifacts": [artifact.to_dict() for artifact in self.artifacts],
|
||||
"exported_source": self.exported_source,
|
||||
"diagnostics": [diagnostic.to_dict() for diagnostic in self.diagnostics],
|
||||
"provenance": [event.to_dict() for event in self.provenance],
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RenderExportAdapter(Protocol):
|
||||
"""Render/export adapter protocol."""
|
||||
|
||||
descriptor: "RenderExportAdapterDescriptor"
|
||||
|
||||
def render(self, request: RenderExportRequest) -> RenderExportResult:
|
||||
"""Run one render/export operation."""
|
||||
|
||||
|
||||
RenderExportAdapterFactory = Any
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RenderExportAdapterDescriptor:
|
||||
"""Inspectable descriptor for one render/export adapter."""
|
||||
|
||||
id: str
|
||||
version: str
|
||||
name: str
|
||||
operations: list[str]
|
||||
input_contracts: list[str]
|
||||
output_profiles: list[str]
|
||||
artifact_media_types: list[str]
|
||||
factory: RenderExportAdapterFactory = field(compare=False, repr=False)
|
||||
summary: str | None = None
|
||||
option_schema: dict[str, Any] = field(default_factory=dict)
|
||||
optional_dependencies: list[OptionalDependency] = field(default_factory=list)
|
||||
safety: dict[str, Any] = field(default_factory=dict)
|
||||
quality_profile: dict[str, Any] = field(default_factory=dict)
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not self.id.strip():
|
||||
raise RenderExportAdapterError("Render/export adapter id cannot be empty")
|
||||
if not self.name.strip():
|
||||
raise RenderExportAdapterError("Render/export adapter name cannot be empty")
|
||||
unsupported_operations = sorted(set(self.operations) - RENDER_OPERATIONS)
|
||||
if unsupported_operations:
|
||||
raise RenderExportAdapterError(
|
||||
"Unsupported render/export operation(s): " + ", ".join(unsupported_operations)
|
||||
)
|
||||
if not self.operations:
|
||||
raise RenderExportAdapterError("Render/export adapter must declare at least one operation")
|
||||
unsupported_profiles = sorted(set(self.output_profiles) - RENDER_PROFILES)
|
||||
if unsupported_profiles:
|
||||
raise RenderExportAdapterError(
|
||||
"Unsupported render/export profile(s): " + ", ".join(unsupported_profiles)
|
||||
)
|
||||
object.__setattr__(self, "artifact_media_types", [value.lower() for value in self.artifact_media_types])
|
||||
|
||||
def instantiate(self) -> RenderExportAdapter:
|
||||
adapter = self.factory()
|
||||
if not isinstance(adapter, RenderExportAdapter):
|
||||
missing = ["render"] if not callable(getattr(adapter, "render", None)) else []
|
||||
if missing:
|
||||
raise RenderExportAdapterError(
|
||||
f"Render/export adapter `{self.id}` is missing method(s): {', '.join(missing)}"
|
||||
)
|
||||
return adapter
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
return _drop_empty(
|
||||
{
|
||||
"id": self.id,
|
||||
"kind": RENDER_EXPORT_ADAPTER_KIND,
|
||||
"version": self.version,
|
||||
"name": self.name,
|
||||
"summary": self.summary,
|
||||
"operations": self.operations,
|
||||
"input_contracts": self.input_contracts,
|
||||
"output_profiles": self.output_profiles,
|
||||
"artifact_media_types": self.artifact_media_types,
|
||||
"option_schema": self.option_schema,
|
||||
"optional_dependencies": [
|
||||
dependency.to_dict() for dependency in self.optional_dependencies
|
||||
],
|
||||
"safety": self.safety,
|
||||
"quality_profile": self.quality_profile,
|
||||
"metadata": self.metadata,
|
||||
"capabilities": [
|
||||
capability.to_dict() for capability in _render_export_capabilities()
|
||||
],
|
||||
"docs": ["docs/render-export-adapters.md"],
|
||||
}
|
||||
)
|
||||
|
||||
def to_extension_descriptor(self) -> ExtensionDescriptor:
|
||||
return ExtensionDescriptor(
|
||||
id=self.id,
|
||||
kind=RENDER_EXPORT_ADAPTER_KIND,
|
||||
version=self.version,
|
||||
summary=self.summary or self.name,
|
||||
capabilities=_render_export_capabilities(),
|
||||
optional_dependencies=self.optional_dependencies,
|
||||
safety=self.safety,
|
||||
input_contract="RenderExportRequest",
|
||||
output_contract="RenderExportResult",
|
||||
diagnostics_namespace="render",
|
||||
provenance_prefix=self.id,
|
||||
docs=["docs/render-export-adapters.md"],
|
||||
examples=["examples/render/fake-render-request.yaml"],
|
||||
metadata={
|
||||
"render_export_adapter": {
|
||||
"name": self.name,
|
||||
"operations": self.operations,
|
||||
"input_contracts": self.input_contracts,
|
||||
"output_profiles": self.output_profiles,
|
||||
"artifact_media_types": self.artifact_media_types,
|
||||
"option_schema": self.option_schema,
|
||||
"quality_profile": self.quality_profile,
|
||||
"metadata": self.metadata,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class RenderExportAdapterRegistry:
|
||||
"""Registry of render/export adapter descriptors."""
|
||||
|
||||
def __init__(self, descriptors: Iterable[RenderExportAdapterDescriptor] | None = None) -> None:
|
||||
self._descriptors: dict[str, RenderExportAdapterDescriptor] = {}
|
||||
for descriptor in descriptors or []:
|
||||
self.register(descriptor)
|
||||
|
||||
def register(self, descriptor: RenderExportAdapterDescriptor) -> None:
|
||||
if descriptor.id in self._descriptors:
|
||||
raise RenderExportAdapterError(f"Duplicate render/export adapter id `{descriptor.id}`")
|
||||
self._descriptors[descriptor.id] = descriptor
|
||||
|
||||
def get(self, adapter_id: str) -> RenderExportAdapterDescriptor:
|
||||
try:
|
||||
return self._descriptors[adapter_id]
|
||||
except KeyError as exc:
|
||||
raise RenderExportAdapterError(f"Unknown render/export adapter `{adapter_id}`") from exc
|
||||
|
||||
def list(self) -> list[RenderExportAdapterDescriptor]:
|
||||
return [self._descriptors[key] for key in sorted(self._descriptors)]
|
||||
|
||||
def check_dependencies(
|
||||
self,
|
||||
adapter_id: str,
|
||||
*,
|
||||
available_modules: set[str] | None = None,
|
||||
) -> ExtensionDependencyCheck:
|
||||
descriptor = self.get(adapter_id)
|
||||
available = (
|
||||
available_modules
|
||||
if available_modules is not None
|
||||
else _available_modules(dependency.name for dependency in descriptor.optional_dependencies)
|
||||
)
|
||||
missing: list[str] = []
|
||||
optional_missing: list[str] = []
|
||||
for dependency in descriptor.optional_dependencies:
|
||||
if dependency.name in available:
|
||||
continue
|
||||
if dependency.required:
|
||||
missing.append(dependency.name)
|
||||
else:
|
||||
optional_missing.append(dependency.name)
|
||||
return ExtensionDependencyCheck(
|
||||
extension_id=adapter_id,
|
||||
missing=missing,
|
||||
optional_missing=optional_missing,
|
||||
)
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
adapters = []
|
||||
for descriptor in self.list():
|
||||
data = descriptor.to_dict()
|
||||
data["dependency_check"] = self.check_dependencies(descriptor.id).to_dict()
|
||||
adapters.append(data)
|
||||
return {"count": len(adapters), "adapters": adapters}
|
||||
|
||||
def extension_descriptors(self) -> list[ExtensionDescriptor]:
|
||||
return [descriptor.to_extension_descriptor() for descriptor in self.list()]
|
||||
|
||||
|
||||
class FakeRenderExportAdapter:
|
||||
"""Deterministic no-op renderer used for contract tests."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.descriptor = fake_render_export_adapter_descriptor()
|
||||
|
||||
def render(self, request: RenderExportRequest) -> RenderExportResult:
|
||||
diagnostics = render_capability_diagnostics(self.descriptor, request)
|
||||
diagnostics.extend(_validate_request(self.descriptor, request))
|
||||
adapter = {
|
||||
"id": self.descriptor.id,
|
||||
"version": self.descriptor.version,
|
||||
"name": self.descriptor.name,
|
||||
}
|
||||
if diagnostics:
|
||||
return RenderExportResult(
|
||||
adapter=adapter,
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
diagnostics=diagnostics,
|
||||
)
|
||||
exported = _fake_exported_source(request)
|
||||
provenance = [
|
||||
RenderProvenance(
|
||||
operation=request.operation,
|
||||
adapter_id=self.descriptor.id,
|
||||
profile=request.profile,
|
||||
source_path=request.source_path,
|
||||
source_digest=_digest_text(request.source),
|
||||
metadata={"deterministic_fake": True},
|
||||
)
|
||||
]
|
||||
if request.operation == "inspect-profile":
|
||||
return RenderExportResult(
|
||||
adapter=adapter,
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
provenance=provenance,
|
||||
metadata={
|
||||
"profile": request.profile,
|
||||
"supported_operations": self.descriptor.operations,
|
||||
"external_renderer_invoked": False,
|
||||
},
|
||||
)
|
||||
if request.operation == "export-source":
|
||||
artifact = RenderArtifact.from_content(
|
||||
exported,
|
||||
role="renderer-source",
|
||||
media_type="text/markdown",
|
||||
metadata={"profile": request.profile},
|
||||
)
|
||||
provenance = [
|
||||
_event_with_artifact(event, artifact.artifact_id)
|
||||
for event in provenance
|
||||
]
|
||||
return RenderExportResult(
|
||||
adapter=adapter,
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
artifacts=[artifact],
|
||||
exported_source=exported,
|
||||
provenance=provenance,
|
||||
metadata={"external_renderer_invoked": False},
|
||||
)
|
||||
rendered = f"FAKE RENDER ARTIFACT\nprofile: {request.profile}\n\n{exported}"
|
||||
artifact = RenderArtifact.from_content(
|
||||
rendered,
|
||||
role="rendered-artifact",
|
||||
media_type=_fake_media_type(request.profile),
|
||||
metadata={"profile": request.profile, "fake_renderer": True},
|
||||
)
|
||||
provenance = [_event_with_artifact(event, artifact.artifact_id) for event in provenance]
|
||||
return RenderExportResult(
|
||||
adapter=adapter,
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
artifacts=[artifact],
|
||||
exported_source=exported,
|
||||
provenance=provenance,
|
||||
metadata={"external_renderer_invoked": False},
|
||||
)
|
||||
|
||||
|
||||
def render_with_adapter(
|
||||
request: RenderExportRequest,
|
||||
*,
|
||||
registry: RenderExportAdapterRegistry | None = None,
|
||||
adapter_id: str = "render.fake",
|
||||
) -> RenderExportResult:
|
||||
"""Render/export through a registered adapter."""
|
||||
|
||||
registry = registry or default_render_export_adapter_registry()
|
||||
descriptor = registry.get(adapter_id)
|
||||
dependency_check = registry.check_dependencies(adapter_id)
|
||||
if not dependency_check.compatible:
|
||||
return RenderExportResult(
|
||||
adapter={"id": descriptor.id, "version": descriptor.version, "name": descriptor.name},
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
diagnostics=[
|
||||
_render_error(
|
||||
"render.missing_dependency",
|
||||
f"Render/export adapter `{descriptor.id}` is missing required dependencies.",
|
||||
details=dependency_check.to_dict(),
|
||||
)
|
||||
],
|
||||
)
|
||||
return descriptor.instantiate().render(request)
|
||||
|
||||
|
||||
def render_capability_diagnostics(
|
||||
descriptor: RenderExportAdapterDescriptor,
|
||||
request: RenderExportRequest,
|
||||
) -> list[Diagnostic]:
|
||||
"""Return diagnostics for capabilities blocked by request policy."""
|
||||
|
||||
policy = request.policy or {}
|
||||
blocked = set(policy.get("blocked_capabilities") or [])
|
||||
denied = []
|
||||
for capability in _render_export_capabilities():
|
||||
if capability.id in blocked:
|
||||
denied.append(capability.id)
|
||||
for safety_key, policy_key in _SAFETY_POLICY_FLAGS.items():
|
||||
if descriptor.safety.get(safety_key) and policy.get(policy_key) is False:
|
||||
denied.append(policy_key)
|
||||
if not denied:
|
||||
return []
|
||||
return [
|
||||
_render_error(
|
||||
"render.capability_blocked",
|
||||
f"Render/export adapter `{descriptor.id}` requires blocked capabilities.",
|
||||
details={"adapter_id": descriptor.id, "capabilities": sorted(set(denied))},
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def default_render_export_adapter_registry() -> RenderExportAdapterRegistry:
|
||||
"""Return the built-in render/export adapter registry."""
|
||||
|
||||
return RenderExportAdapterRegistry([*discover_render_export_adapters(), fake_render_export_adapter_descriptor()])
|
||||
|
||||
|
||||
def discover_render_export_adapters() -> list[RenderExportAdapterDescriptor]:
|
||||
"""Discover package-provided render/export adapter descriptors."""
|
||||
|
||||
descriptors: list[RenderExportAdapterDescriptor] = []
|
||||
for entry_point in _entry_points():
|
||||
try:
|
||||
descriptors.extend(_normalize_entry_point_result(entry_point.load()))
|
||||
except Exception as exc: # pragma: no cover - defensive boundary
|
||||
descriptors.append(_failed_discovery_descriptor(entry_point.name, exc))
|
||||
return descriptors
|
||||
|
||||
|
||||
def render_export_registry_descriptor() -> ExtensionDescriptor:
|
||||
"""Descriptor for the render/export adapter registry itself."""
|
||||
|
||||
return ExtensionDescriptor(
|
||||
id="render.export-registry",
|
||||
kind="render-export-registry",
|
||||
summary="Registry and descriptor contract for optional render/export adapters.",
|
||||
capabilities=[
|
||||
ProcessingCapability(id="render_export_adapters", kind="discover"),
|
||||
ProcessingCapability(id="diagnostics", kind="emit"),
|
||||
ProcessingCapability(id="provenance", kind="emit"),
|
||||
],
|
||||
safety={"network": False, "external_process": False, "filesystem_write": False},
|
||||
input_contract="RenderExportAdapterDescriptor entry points",
|
||||
output_contract="RenderExportAdapterRegistry",
|
||||
diagnostics_namespace="render",
|
||||
provenance_prefix="render.export_registry",
|
||||
docs=["docs/render-export-adapters.md"],
|
||||
examples=["examples/render/fake-render-request.yaml"],
|
||||
metadata={
|
||||
"entry_point_group": RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
|
||||
"schema_version": RENDER_EXPORT_SCHEMA_VERSION,
|
||||
"concrete_renderer_execution_required": False,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def fake_render_export_adapter_descriptor() -> RenderExportAdapterDescriptor:
|
||||
"""Descriptor for the built-in fake render/export adapter."""
|
||||
|
||||
return RenderExportAdapterDescriptor(
|
||||
id="render.fake",
|
||||
version="1",
|
||||
name="Deterministic Fake Renderer",
|
||||
summary="Fake renderer for render/export contract tests; never invokes external processes.",
|
||||
operations=["inspect-profile", "export-source", "render-artifact"],
|
||||
input_contracts=["Markdown", "DocumentFunctionEvaluationResult"],
|
||||
output_profiles=["plain", "docs", "slides", "paged", "static-site", "pdf"],
|
||||
artifact_media_types=["text/plain", "text/markdown"],
|
||||
factory=FakeRenderExportAdapter,
|
||||
safety={
|
||||
"filesystem_read": False,
|
||||
"filesystem_write": False,
|
||||
"external_process": False,
|
||||
"network": False,
|
||||
"native_renderer_dependency": False,
|
||||
"assisted_generation": False,
|
||||
"publication_side_effect": False,
|
||||
},
|
||||
quality_profile={"deterministic_fake": True, "layout_fidelity": "none"},
|
||||
metadata={
|
||||
"external_renderer_invoked": False,
|
||||
"quarkdown_dependency": False,
|
||||
"contract_fixture": True,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _validate_request(
|
||||
descriptor: RenderExportAdapterDescriptor,
|
||||
request: RenderExportRequest,
|
||||
) -> list[Diagnostic]:
|
||||
diagnostics: list[Diagnostic] = []
|
||||
if request.schema_version != RENDER_EXPORT_SCHEMA_VERSION:
|
||||
diagnostics.append(
|
||||
_render_error(
|
||||
"render.schema_version",
|
||||
f"Expected schema_version `{RENDER_EXPORT_SCHEMA_VERSION}`.",
|
||||
severity="warning",
|
||||
details={"actual": request.schema_version},
|
||||
)
|
||||
)
|
||||
if request.operation not in descriptor.operations:
|
||||
diagnostics.append(
|
||||
_render_error(
|
||||
"render.operation_unsupported",
|
||||
f"Adapter `{descriptor.id}` does not support operation `{request.operation}`.",
|
||||
details={"adapter_id": descriptor.id, "operation": request.operation},
|
||||
)
|
||||
)
|
||||
if request.profile not in descriptor.output_profiles:
|
||||
diagnostics.append(
|
||||
_render_error(
|
||||
"render.profile_unsupported",
|
||||
f"Adapter `{descriptor.id}` does not support profile `{request.profile}`.",
|
||||
details={"adapter_id": descriptor.id, "profile": request.profile},
|
||||
)
|
||||
)
|
||||
if not request.source and request.operation != "inspect-profile":
|
||||
diagnostics.append(_render_error("render.source_missing", "Render/export request source cannot be empty."))
|
||||
return diagnostics
|
||||
|
||||
|
||||
def _render_export_capabilities() -> list[ProcessingCapability]:
|
||||
return [
|
||||
ProcessingCapability(id="render_export", kind="execute"),
|
||||
ProcessingCapability(id="renderer_source", kind="emit"),
|
||||
ProcessingCapability(id="render_artifact", kind="emit"),
|
||||
ProcessingCapability(id="diagnostics", kind="emit"),
|
||||
ProcessingCapability(id="provenance", kind="emit"),
|
||||
]
|
||||
|
||||
|
||||
def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderProvenance:
|
||||
return RenderProvenance(
|
||||
operation=event.operation,
|
||||
adapter_id=event.adapter_id,
|
||||
profile=event.profile,
|
||||
source_path=event.source_path,
|
||||
source_digest=event.source_digest,
|
||||
artifact_id=artifact_id,
|
||||
metadata=event.metadata,
|
||||
)
|
||||
|
||||
|
||||
def _fake_exported_source(request: RenderExportRequest) -> str:
|
||||
return (
|
||||
f"<!-- render.fake profile={request.profile} operation={request.operation} -->\n\n"
|
||||
+ request.source
|
||||
)
|
||||
|
||||
|
||||
def _fake_media_type(profile: str) -> str:
|
||||
if profile == "pdf":
|
||||
return "text/plain"
|
||||
if profile in {"docs", "static-site"}:
|
||||
return "text/html"
|
||||
return "text/plain"
|
||||
|
||||
|
||||
def _digest_text(value: str) -> str:
|
||||
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
def _available_modules(module_names: Iterable[str]) -> set[str]:
|
||||
import importlib.util
|
||||
|
||||
return {
|
||||
module_name
|
||||
for module_name in module_names
|
||||
if importlib.util.find_spec(module_name) is not None
|
||||
}
|
||||
|
||||
|
||||
def _entry_points() -> list[Any]:
|
||||
entry_points = importlib.metadata.entry_points()
|
||||
if hasattr(entry_points, "select"):
|
||||
return list(entry_points.select(group=RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP))
|
||||
return list(entry_points.get(RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP, []))
|
||||
|
||||
|
||||
def _normalize_entry_point_result(loaded: Any) -> list[RenderExportAdapterDescriptor]:
|
||||
value = loaded() if callable(loaded) and not isinstance(loaded, RenderExportAdapterDescriptor) else loaded
|
||||
if isinstance(value, RenderExportAdapterDescriptor):
|
||||
return [value]
|
||||
if isinstance(value, Iterable) and not isinstance(value, (str, bytes, dict)):
|
||||
descriptors = list(value)
|
||||
if all(isinstance(descriptor, RenderExportAdapterDescriptor) for descriptor in descriptors):
|
||||
return descriptors
|
||||
raise RenderExportAdapterError("Render/export adapter entry point must return descriptor objects")
|
||||
|
||||
|
||||
def _failed_discovery_descriptor(name: str, exc: Exception) -> RenderExportAdapterDescriptor:
|
||||
def factory() -> RenderExportAdapter:
|
||||
return _FailedRenderExportAdapter(name, exc)
|
||||
|
||||
return RenderExportAdapterDescriptor(
|
||||
id=f"render.discovery-failed.{name}",
|
||||
version="0",
|
||||
name=f"Failed render/export adapter: {name}",
|
||||
operations=["inspect-profile"],
|
||||
input_contracts=["RenderExportRequest"],
|
||||
output_profiles=["plain"],
|
||||
artifact_media_types=[],
|
||||
factory=factory,
|
||||
summary="Failed render/export adapter discovery placeholder.",
|
||||
safety={"filesystem_read": False, "filesystem_write": False, "network": False},
|
||||
metadata={"error": str(exc)},
|
||||
)
|
||||
|
||||
|
||||
class _FailedRenderExportAdapter:
|
||||
def __init__(self, name: str, exc: Exception) -> None:
|
||||
self.descriptor = _failed_discovery_descriptor(name, exc)
|
||||
self._error = exc
|
||||
|
||||
def render(self, request: RenderExportRequest) -> RenderExportResult:
|
||||
return RenderExportResult(
|
||||
adapter={"id": self.descriptor.id, "version": self.descriptor.version, "name": self.descriptor.name},
|
||||
operation=request.operation,
|
||||
profile=request.profile,
|
||||
diagnostics=[
|
||||
_render_error(
|
||||
"render.discovery_failed",
|
||||
f"Render/export adapter `{self.descriptor.id}` failed during discovery.",
|
||||
details={"error": str(self._error)},
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
def _render_error(
|
||||
code: str,
|
||||
message: str,
|
||||
*,
|
||||
severity: str = "error",
|
||||
details: dict[str, Any] | None = None,
|
||||
) -> Diagnostic:
|
||||
return Diagnostic(
|
||||
severity=severity,
|
||||
code=code,
|
||||
message=message,
|
||||
source=SourceLocation(path="<render>"),
|
||||
details=details or {},
|
||||
)
|
||||
|
||||
|
||||
def _drop_empty(data: dict[str, Any]) -> dict[str, Any]:
|
||||
return {
|
||||
key: value
|
||||
for key, value in data.items()
|
||||
if value not in (None, [], {}, "")
|
||||
}
|
||||
@@ -24,6 +24,8 @@ def test_builtin_extension_registry_lists_query_processors_and_backend():
|
||||
assert "memory.graph-contract" in ids
|
||||
assert "memory.runtime-adapter-boundary" in ids
|
||||
assert "memory.context-package" in ids
|
||||
assert "render.export-registry" in ids
|
||||
assert "render.fake" in ids
|
||||
assert "source.adapter-registry" in ids
|
||||
|
||||
|
||||
@@ -182,3 +184,18 @@ def test_builtin_memory_graph_descriptor_exposes_runtime_handoff_boundaries():
|
||||
assert adapters.safety["runtime_execution"] is False
|
||||
assert adapters.metadata["services_launched_by_markitect_tool"] is False
|
||||
assert "examples/memory/runtime-adapter-boundaries.yaml" in adapters.examples
|
||||
|
||||
|
||||
def test_builtin_render_export_descriptors_expose_contract_boundary():
|
||||
registry = builtin_extension_registry()
|
||||
|
||||
registry_descriptor = registry.get("render.export-registry")
|
||||
fake = registry.get("render.fake")
|
||||
|
||||
assert registry_descriptor.kind == "render-export-registry"
|
||||
assert registry_descriptor.safety["external_process"] is False
|
||||
assert registry_descriptor.metadata["concrete_renderer_execution_required"] is False
|
||||
assert fake.kind == "render-export"
|
||||
assert fake.safety["external_process"] is False
|
||||
assert fake.safety["filesystem_write"] is False
|
||||
assert fake.metadata["render_export_adapter"]["metadata"]["quarkdown_dependency"] is False
|
||||
|
||||
107
tests/test_render_export_contract.py
Normal file
107
tests/test_render_export_contract.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from markitect_tool.diagnostics import has_error
|
||||
from markitect_tool.render import (
|
||||
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
|
||||
RENDER_EXPORT_SCHEMA_VERSION,
|
||||
RenderExportAdapterDescriptor,
|
||||
RenderExportAdapterRegistry,
|
||||
RenderExportRequest,
|
||||
default_render_export_adapter_registry,
|
||||
render_capability_diagnostics,
|
||||
render_export_registry_descriptor,
|
||||
render_with_adapter,
|
||||
)
|
||||
|
||||
|
||||
def test_render_export_registry_lists_fake_adapter_and_serializes_descriptor():
|
||||
registry = default_render_export_adapter_registry()
|
||||
|
||||
descriptor = registry.get("render.fake")
|
||||
data = descriptor.to_dict()
|
||||
|
||||
assert data["kind"] == "render-export"
|
||||
assert data["operations"] == ["inspect-profile", "export-source", "render-artifact"]
|
||||
assert "pdf" in data["output_profiles"]
|
||||
assert registry.to_dict()["count"] >= 1
|
||||
|
||||
|
||||
def test_fake_render_export_adapter_exports_source_and_provenance():
|
||||
request = RenderExportRequest(
|
||||
source="# Demo\n\nBody.",
|
||||
operation="export-source",
|
||||
profile="docs",
|
||||
source_path="docs/demo.md",
|
||||
)
|
||||
|
||||
result = render_with_adapter(request)
|
||||
|
||||
assert result.valid
|
||||
assert result.schema_version == RENDER_EXPORT_SCHEMA_VERSION
|
||||
assert result.exported_source.startswith("<!-- render.fake profile=docs")
|
||||
assert result.artifacts[0].role == "renderer-source"
|
||||
assert result.artifacts[0].media_type == "text/markdown"
|
||||
assert result.provenance[0].source_path == "docs/demo.md"
|
||||
assert result.provenance[0].artifact_id == result.artifacts[0].artifact_id
|
||||
|
||||
|
||||
def test_fake_render_export_adapter_renders_artifact_without_external_renderer():
|
||||
request = RenderExportRequest(source="# Demo", operation="render-artifact", profile="pdf")
|
||||
|
||||
result = render_with_adapter(request)
|
||||
|
||||
assert result.valid
|
||||
assert result.artifacts[0].role == "rendered-artifact"
|
||||
assert result.artifacts[0].digest.startswith("sha256:")
|
||||
assert result.metadata["external_renderer_invoked"] is False
|
||||
|
||||
|
||||
def test_render_export_request_validation_reports_unsupported_profile():
|
||||
request = RenderExportRequest(source="# Demo", operation="render-artifact", profile="epub")
|
||||
|
||||
result = render_with_adapter(request)
|
||||
|
||||
assert not result.valid
|
||||
assert result.diagnostics[0].code == "render.profile_unsupported"
|
||||
|
||||
|
||||
def test_render_capability_policy_blocks_declared_safety_flags():
|
||||
descriptor = RenderExportAdapterDescriptor(
|
||||
id="render.external",
|
||||
version="1",
|
||||
name="External Renderer",
|
||||
operations=["render-artifact"],
|
||||
input_contracts=["Markdown"],
|
||||
output_profiles=["pdf"],
|
||||
artifact_media_types=["application/pdf"],
|
||||
factory=lambda: object(),
|
||||
safety={"external_process": True, "filesystem_write": True},
|
||||
)
|
||||
request = RenderExportRequest(
|
||||
source="# Demo",
|
||||
operation="render-artifact",
|
||||
profile="pdf",
|
||||
policy={"external_process": False, "filesystem_write": False},
|
||||
)
|
||||
|
||||
diagnostics = render_capability_diagnostics(descriptor, request)
|
||||
|
||||
assert len(diagnostics) == 1
|
||||
assert diagnostics[0].code == "render.capability_blocked"
|
||||
assert diagnostics[0].details["capabilities"] == ["external_process", "filesystem_write"]
|
||||
|
||||
|
||||
def test_render_export_registry_descriptor_points_to_entry_point_group():
|
||||
descriptor = render_export_registry_descriptor()
|
||||
|
||||
assert descriptor.id == "render.export-registry"
|
||||
assert descriptor.kind == "render-export-registry"
|
||||
assert descriptor.metadata["entry_point_group"] == RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP
|
||||
assert descriptor.metadata["concrete_renderer_execution_required"] is False
|
||||
|
||||
|
||||
def test_render_result_validity_tracks_error_diagnostics():
|
||||
request = RenderExportRequest(source="", operation="render-artifact", profile="plain")
|
||||
|
||||
result = render_with_adapter(request)
|
||||
|
||||
assert has_error(result.diagnostics)
|
||||
assert not result.valid
|
||||
@@ -3,10 +3,10 @@ id: MKTT-WP-0020
|
||||
type: workplan
|
||||
title: "Render Export Adapter Contract"
|
||||
domain: markitect
|
||||
status: todo
|
||||
status: done
|
||||
owner: markitect-tool
|
||||
topic_slug: markitect
|
||||
planning_priority: P2
|
||||
planning_priority: complete
|
||||
planning_order: 150
|
||||
depends_on_workplans:
|
||||
- MKTT-WP-0013
|
||||
@@ -55,11 +55,29 @@ renderer-specific sources. The two directions must stay separate now that
|
||||
|
||||
Concrete Quarkdown integration belongs in `markitect-quarkdown`.
|
||||
|
||||
## Implementation Summary - 2026-05-15
|
||||
|
||||
Implemented the contract-only render/export adapter layer:
|
||||
|
||||
- `RenderExportAdapterDescriptor`, `RenderExportAdapterRegistry`, and optional
|
||||
package discovery through `markitect_tool.render_export_adapters`.
|
||||
- `RenderExportRequest`, `RenderExportResult`, `RenderArtifact`, and
|
||||
`RenderProvenance` envelopes.
|
||||
- Built-in deterministic `render.fake` adapter for contract tests.
|
||||
- Capability and safety diagnostics for filesystem, network, external process,
|
||||
native renderer dependency, assisted generation, and publication side-effect
|
||||
boundaries.
|
||||
- Extension catalog descriptors for `render.export-registry` and `render.fake`.
|
||||
- Docs, fixture, public API exports, generated API reference, and tests.
|
||||
|
||||
No real renderer, Quarkdown invocation, external process, or filesystem-writing
|
||||
publication behavior was added to `markitect-tool`.
|
||||
|
||||
## P20.1 - Define render adapter descriptors
|
||||
|
||||
```task
|
||||
id: MKTT-WP-0020-T001
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "5b52b196-a7f5-4e4f-abc6-972febdc2638"
|
||||
```
|
||||
@@ -86,7 +104,7 @@ registry tests.
|
||||
|
||||
```task
|
||||
id: MKTT-WP-0020-T002
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "3d43b168-7e55-4885-ae17-fcea262f2641"
|
||||
```
|
||||
@@ -108,7 +126,7 @@ Output: serializable models, round-trip tests, and docs.
|
||||
|
||||
```task
|
||||
id: MKTT-WP-0020-T003
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "45748ad7-131c-4b75-9720-6958fde93208"
|
||||
```
|
||||
@@ -130,7 +148,7 @@ Output: fake adapter, tests, and examples.
|
||||
|
||||
```task
|
||||
id: MKTT-WP-0020-T004
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "50aa8f00-eeba-495d-9d45-089f855fc3bd"
|
||||
```
|
||||
@@ -154,7 +172,7 @@ Output: capability constants, blocked-operation diagnostics, and tests.
|
||||
|
||||
```task
|
||||
id: MKTT-WP-0020-T005
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
state_hub_task_id: "173a75d0-d82c-4dcb-9ced-eb73e9438db2"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user