generated from coulomb/repo-seed
Add render export adapter contract
This commit is contained in:
@@ -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, [], {}, "")
|
||||
}
|
||||
Reference in New Issue
Block a user