Add render reference asset manifest contract

This commit is contained in:
2026-05-15 14:08:26 +02:00
parent 03e5ae00cc
commit 305d75177a
14 changed files with 1405 additions and 20 deletions

View File

@@ -263,8 +263,16 @@ from markitect_tool.render import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION,
RENDER_ASSET_COPY_POLICIES,
RENDER_REFERENCE_MANIFEST_KIND,
RENDER_REFERENCE_SCHEMA_VERSION,
RENDER_UNIT_KINDS,
FakeRenderExportAdapter,
RenderArtifact,
RenderAsset,
RenderAssetManifest,
RenderAssetProvenance,
RenderCrossReference,
RenderExportAdapter,
RenderExportAdapterDescriptor,
RenderExportAdapterError,
@@ -272,10 +280,20 @@ from markitect_tool.render import (
RenderExportRequest,
RenderExportResult,
RenderProvenance,
RenderReferenceError,
RenderReferenceManifest,
RenderSourceMap,
RenderSourceSpan,
RenderTocEntry,
RenderUnitReference,
default_render_export_adapter_registry,
discover_render_export_adapters,
render_capability_diagnostics,
render_asset_id,
render_export_registry_descriptor,
render_manifest_id,
render_reference_manifest_descriptor,
render_unit_id,
render_with_adapter,
)
from markitect_tool.schema import (
@@ -592,8 +610,16 @@ __all__ = [
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION",
"RENDER_ASSET_COPY_POLICIES",
"RENDER_REFERENCE_MANIFEST_KIND",
"RENDER_REFERENCE_SCHEMA_VERSION",
"RENDER_UNIT_KINDS",
"FakeRenderExportAdapter",
"RenderArtifact",
"RenderAsset",
"RenderAssetManifest",
"RenderAssetProvenance",
"RenderCrossReference",
"RenderExportAdapter",
"RenderExportAdapterDescriptor",
"RenderExportAdapterError",
@@ -601,10 +627,20 @@ __all__ = [
"RenderExportRequest",
"RenderExportResult",
"RenderProvenance",
"RenderReferenceError",
"RenderReferenceManifest",
"RenderSourceMap",
"RenderSourceSpan",
"RenderTocEntry",
"RenderUnitReference",
"default_render_export_adapter_registry",
"discover_render_export_adapters",
"render_capability_diagnostics",
"render_asset_id",
"render_export_registry_descriptor",
"render_manifest_id",
"render_reference_manifest_descriptor",
"render_unit_id",
"render_with_adapter",
"MissingTemplateVariable",
"TemplateAnalysis",

View File

@@ -8,6 +8,7 @@ from markitect_tool.query import default_query_engine_registry
from markitect_tool.render import (
default_render_export_adapter_registry,
render_export_registry_descriptor,
render_reference_manifest_descriptor,
)
from markitect_tool.source import (
default_source_adapter_registry,
@@ -33,6 +34,7 @@ def builtin_extension_registry() -> ExtensionRegistry:
_memory_runtime_adapter_descriptor(),
_agent_memory_descriptor(),
render_export_registry_descriptor(),
render_reference_manifest_descriptor(),
source_adapter_registry_descriptor(),
]:
registry.register(descriptor)

View File

@@ -4,8 +4,16 @@ from markitect_tool.render.engine import (
RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP,
RENDER_EXPORT_ADAPTER_KIND,
RENDER_EXPORT_SCHEMA_VERSION,
RENDER_ASSET_COPY_POLICIES,
RENDER_REFERENCE_MANIFEST_KIND,
RENDER_REFERENCE_SCHEMA_VERSION,
RENDER_UNIT_KINDS,
FakeRenderExportAdapter,
RenderArtifact,
RenderAsset,
RenderAssetManifest,
RenderAssetProvenance,
RenderCrossReference,
RenderExportAdapter,
RenderExportAdapterDescriptor,
RenderExportAdapterError,
@@ -13,10 +21,20 @@ from markitect_tool.render.engine import (
RenderExportRequest,
RenderExportResult,
RenderProvenance,
RenderReferenceError,
RenderReferenceManifest,
RenderSourceMap,
RenderSourceSpan,
RenderTocEntry,
RenderUnitReference,
default_render_export_adapter_registry,
discover_render_export_adapters,
render_capability_diagnostics,
render_asset_id,
render_export_registry_descriptor,
render_manifest_id,
render_reference_manifest_descriptor,
render_unit_id,
render_with_adapter,
)
@@ -24,8 +42,16 @@ __all__ = [
"RENDER_EXPORT_ADAPTER_ENTRY_POINT_GROUP",
"RENDER_EXPORT_ADAPTER_KIND",
"RENDER_EXPORT_SCHEMA_VERSION",
"RENDER_ASSET_COPY_POLICIES",
"RENDER_REFERENCE_MANIFEST_KIND",
"RENDER_REFERENCE_SCHEMA_VERSION",
"RENDER_UNIT_KINDS",
"FakeRenderExportAdapter",
"RenderArtifact",
"RenderAsset",
"RenderAssetManifest",
"RenderAssetProvenance",
"RenderCrossReference",
"RenderExportAdapter",
"RenderExportAdapterDescriptor",
"RenderExportAdapterError",
@@ -33,9 +59,19 @@ __all__ = [
"RenderExportRequest",
"RenderExportResult",
"RenderProvenance",
"RenderReferenceError",
"RenderReferenceManifest",
"RenderSourceMap",
"RenderSourceSpan",
"RenderTocEntry",
"RenderUnitReference",
"default_render_export_adapter_registry",
"discover_render_export_adapters",
"render_capability_diagnostics",
"render_asset_id",
"render_export_registry_descriptor",
"render_manifest_id",
"render_reference_manifest_descriptor",
"render_unit_id",
"render_with_adapter",
]

View File

@@ -37,6 +37,26 @@ RENDER_PROFILES = {
"pdf",
}
RENDER_REFERENCE_SCHEMA_VERSION = "markitect.render.reference.v1"
RENDER_REFERENCE_MANIFEST_KIND = "render-reference-manifest"
RENDER_UNIT_KINDS = {
"figure",
"table",
"equation",
"code-block",
"section",
"custom",
}
RENDER_ASSET_COPY_POLICIES = {
"copy",
"embed",
"link",
"preserve",
"skip",
}
_SAFETY_POLICY_FLAGS = {
"filesystem_read": "filesystem_read",
"filesystem_write": "filesystem_write",
@@ -52,6 +72,657 @@ class RenderExportAdapterError(ValueError):
"""Raised when render/export adapter contracts are invalid."""
class RenderReferenceError(ValueError):
"""Raised when render reference or asset manifest contracts are invalid."""
@dataclass(frozen=True)
class RenderSourceSpan:
"""Source location for a render unit, asset, or source-map edge."""
source_path: str | None = None
line_start: int | None = None
line_end: int | None = None
selector: str | None = None
source_unit_id: str | None = None
content_hash: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if (
self.line_start is not None
and self.line_end is not None
and self.line_start > self.line_end
):
raise RenderReferenceError("Render source span line_start cannot be after line_end")
def to_dict(self) -> dict[str, Any]:
return _drop_empty(asdict(self))
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "RenderSourceSpan | None":
if not data:
return None
return cls(
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
line_start=int(data["line_start"]) if data.get("line_start") is not None else None,
line_end=int(data["line_end"]) if data.get("line_end") is not None else None,
selector=str(data["selector"]) if data.get("selector") is not None else None,
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderUnitReference:
"""Stable identity for one renderable unit before final renderer numbering."""
unit_id: str = ""
kind: str = "custom"
label: str | None = None
title: str | None = None
caption: str | None = None
source_path: str | None = None
anchor: str | None = None
source_span: RenderSourceSpan | None = None
content_hash: str | None = None
ordinal_hint: int | None = None
numbering: dict[str, Any] = field(default_factory=dict)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
kind = self.kind.strip()
if kind not in RENDER_UNIT_KINDS:
raise RenderReferenceError(f"Unsupported render unit kind `{self.kind}`")
if self.ordinal_hint is not None and self.ordinal_hint < 0:
raise RenderReferenceError("Render unit ordinal_hint cannot be negative")
if not self.unit_id:
object.__setattr__(
self,
"unit_id",
render_unit_id(
kind,
source_path=self.source_path,
anchor=self.anchor,
content_hash=self.content_hash,
ordinal_hint=self.ordinal_hint,
title=self.title or self.caption,
),
)
if self.source_span and self.source_span.source_path and not self.source_path:
object.__setattr__(self, "source_path", self.source_span.source_path)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"unit_id": self.unit_id,
"kind": self.kind,
"label": self.label,
"title": self.title,
"caption": self.caption,
"source_path": self.source_path,
"anchor": self.anchor,
"source_span": self.source_span.to_dict() if self.source_span else None,
"content_hash": self.content_hash,
"ordinal_hint": self.ordinal_hint,
"numbering": self.numbering,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderUnitReference":
return cls(
unit_id=str(data.get("unit_id", "")),
kind=str(data.get("kind", "custom")),
label=str(data["label"]) if data.get("label") is not None else None,
title=str(data["title"]) if data.get("title") is not None else None,
caption=str(data["caption"]) if data.get("caption") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
anchor=str(data["anchor"]) if data.get("anchor") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
ordinal_hint=int(data["ordinal_hint"]) if data.get("ordinal_hint") is not None else None,
numbering=dict(data.get("numbering", {})),
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_content_unit(
cls,
unit: Any,
*,
kind: str | None = None,
label: str | None = None,
) -> "RenderUnitReference":
data = _mapping_from(unit)
span_data = data.get("span") if isinstance(data.get("span"), dict) else {}
source_path = str(data["source_path"]) if data.get("source_path") is not None else None
return cls(
unit_id=str(data.get("unit_id", "")),
kind=kind or _render_kind_from_content_unit(str(data.get("kind", "custom"))),
label=label,
title=str(data["name"]) if data.get("name") is not None else None,
source_path=source_path,
source_span=RenderSourceSpan(
source_path=source_path,
line_start=int(span_data["line_start"]) if span_data.get("line_start") is not None else None,
line_end=int(span_data["line_end"]) if span_data.get("line_end") is not None else None,
source_unit_id=str(data.get("unit_id", "")) or None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
)
if source_path or span_data
else None,
content_hash=str(data["content_hash"]) if data.get("content_hash") is not None else None,
metadata={"content_unit_kind": data.get("kind")} if data.get("kind") else {},
)
@dataclass(frozen=True)
class RenderCrossReference:
"""Requested cross-reference link before renderer-specific numbering exists."""
target_unit_id: str
reference_id: str = ""
source_unit_id: str | None = None
source_span: RenderSourceSpan | None = None
label: str | None = None
requested_style: str = "numbered"
fallback_text: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.target_unit_id.strip():
raise RenderReferenceError("Cross-reference target_unit_id cannot be empty")
if not self.reference_id:
object.__setattr__(
self,
"reference_id",
_stable_id(
"xref",
self.source_unit_id,
self.target_unit_id,
self.source_span.to_dict() if self.source_span else None,
self.requested_style,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"reference_id": self.reference_id,
"source_unit_id": self.source_unit_id,
"target_unit_id": self.target_unit_id,
"source_span": self.source_span.to_dict() if self.source_span else None,
"label": self.label,
"requested_style": self.requested_style,
"fallback_text": self.fallback_text,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderCrossReference":
return cls(
target_unit_id=str(data["target_unit_id"]),
reference_id=str(data.get("reference_id", "")),
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
label=str(data["label"]) if data.get("label") is not None else None,
requested_style=str(data.get("requested_style", "numbered")),
fallback_text=str(data["fallback_text"]) if data.get("fallback_text") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderTocEntry:
"""Table-of-contents planning entry before final renderer links exist."""
unit_id: str
title: str
level: int
entry_id: str = ""
parent_id: str | None = None
order: int | None = None
source_span: RenderSourceSpan | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not self.unit_id.strip():
raise RenderReferenceError("TOC unit_id cannot be empty")
if not self.title.strip():
raise RenderReferenceError("TOC title cannot be empty")
if self.level < 1:
raise RenderReferenceError("TOC level must be greater than zero")
if self.order is not None and self.order < 0:
raise RenderReferenceError("TOC order cannot be negative")
if not self.entry_id:
object.__setattr__(self, "entry_id", _stable_id("toc", self.unit_id, self.title, self.level))
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"entry_id": self.entry_id,
"unit_id": self.unit_id,
"title": self.title,
"level": self.level,
"parent_id": self.parent_id,
"order": self.order,
"source_span": self.source_span.to_dict() if self.source_span else None,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderTocEntry":
return cls(
unit_id=str(data["unit_id"]),
title=str(data["title"]),
level=int(data["level"]),
entry_id=str(data.get("entry_id", "")),
parent_id=str(data["parent_id"]) if data.get("parent_id") is not None else None,
order=int(data["order"]) if data.get("order") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAssetProvenance:
"""Provenance for an asset manifest entry."""
source_uri: str | None = None
source_path: str | None = None
source_href: str | None = None
package_path: str | None = None
attachment_id: str | None = None
source_adapter_id: str | None = None
source_span: RenderSourceSpan | None = None
digest: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"source_uri": self.source_uri,
"source_path": self.source_path,
"source_href": self.source_href,
"package_path": self.package_path,
"attachment_id": self.attachment_id,
"source_adapter_id": self.source_adapter_id,
"source_span": self.source_span.to_dict() if self.source_span else None,
"digest": self.digest,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderAssetProvenance":
return cls(
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_href=str(data["source_href"]) if data.get("source_href") is not None else None,
package_path=str(data["package_path"]) if data.get("package_path") is not None else None,
attachment_id=str(data["attachment_id"]) if data.get("attachment_id") is not None else None,
source_adapter_id=str(data["source_adapter_id"]) if data.get("source_adapter_id") is not None else None,
source_span=RenderSourceSpan.from_dict(data.get("source_span")),
digest=str(data["digest"]) if data.get("digest") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_source_provenance(
cls,
provenance: Any,
*,
attachment_id: str | None = None,
source_adapter_id: str | None = None,
) -> "RenderAssetProvenance":
data = _mapping_from(provenance)
return cls(
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_href=str(data["source_href"]) if data.get("source_href") is not None else None,
package_path=str(data["package_path"]) if data.get("package_path") is not None else None,
attachment_id=attachment_id,
source_adapter_id=source_adapter_id,
source_span=RenderSourceSpan(
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
selector=str(data["anchor"]) if data.get("anchor") is not None else None,
metadata={
key: str(data[key])
for key in ("page", "section", "start_offset", "end_offset")
if data.get(key) is not None
},
),
digest=str(data["digest"]) if data.get("digest") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAsset:
"""Static asset descriptor with declared renderer copy behavior."""
asset_id: str = ""
source_uri: str | None = None
source_path: str | None = None
name: str | None = None
media_type: str | None = None
extension: str | None = None
digest: str | None = None
role: str = "asset"
copy_policy: str = "copy"
output_reference: str | None = None
provenance: list[RenderAssetProvenance] = field(default_factory=list)
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not (self.source_uri or self.source_path):
raise RenderReferenceError("Render asset needs source_uri or source_path")
if not self.role.strip():
raise RenderReferenceError("Render asset role cannot be empty")
if self.copy_policy not in RENDER_ASSET_COPY_POLICIES:
raise RenderReferenceError(f"Unsupported render asset copy_policy `{self.copy_policy}`")
if not self.asset_id:
object.__setattr__(
self,
"asset_id",
render_asset_id(
self.source_uri or self.source_path or "",
digest=self.digest,
role=self.role,
output_reference=self.output_reference,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"asset_id": self.asset_id,
"source_uri": self.source_uri,
"source_path": self.source_path,
"name": self.name,
"media_type": self.media_type,
"extension": self.extension,
"digest": self.digest,
"role": self.role,
"copy_policy": self.copy_policy,
"output_reference": self.output_reference,
"provenance": [event.to_dict() for event in self.provenance],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderAsset":
return cls(
asset_id=str(data.get("asset_id", "")),
source_uri=str(data["source_uri"]) if data.get("source_uri") is not None else None,
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
name=str(data["name"]) if data.get("name") is not None else None,
media_type=str(data["media_type"]) if data.get("media_type") is not None else None,
extension=str(data["extension"]) if data.get("extension") is not None else None,
digest=str(data["digest"]) if data.get("digest") is not None else None,
role=str(data.get("role", "asset")),
copy_policy=str(data.get("copy_policy", "copy")),
output_reference=str(data["output_reference"]) if data.get("output_reference") is not None else None,
provenance=[RenderAssetProvenance.from_dict(event) for event in data.get("provenance", [])],
metadata=dict(data.get("metadata", {})),
)
@classmethod
def from_source_asset(
cls,
asset: Any,
*,
role: str = "attachment",
copy_policy: str = "copy",
output_reference: str | None = None,
provenance: list[RenderAssetProvenance] | None = None,
) -> "RenderAsset":
data = _mapping_from(asset)
source_uri = str(data.get("uri") or data.get("source_uri") or "")
source_path = str(data["path"]) if data.get("path") is not None else None
digest = str(data["digest"]) if data.get("digest") is not None else None
resolved_provenance = provenance
if resolved_provenance is None:
resolved_provenance = [
RenderAssetProvenance(
source_uri=source_uri or None,
source_path=source_path,
attachment_id=str(data["name"]) if data.get("name") is not None else None,
digest=digest,
metadata={"source_asset": True},
)
]
return cls(
source_uri=source_uri or None,
source_path=source_path,
name=str(data["name"]) if data.get("name") is not None else None,
media_type=str(data["media_type"]) if data.get("media_type") is not None else None,
extension=str(data["extension"]) if data.get("extension") is not None else None,
digest=digest,
role=role,
copy_policy=copy_policy,
output_reference=output_reference,
provenance=resolved_provenance,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderAssetManifest:
"""Deterministic list of static assets requested by renderer source."""
manifest_id: str = ""
assets: list[RenderAsset] = field(default_factory=list)
source_path: str | None = None
source_digest: str | None = None
schema_version: str = RENDER_REFERENCE_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.schema_version != RENDER_REFERENCE_SCHEMA_VERSION:
raise RenderReferenceError(f"Expected schema_version `{RENDER_REFERENCE_SCHEMA_VERSION}`")
assets = sorted(self.assets, key=lambda asset: asset.asset_id)
_check_duplicates([asset.asset_id for asset in assets], "asset")
object.__setattr__(self, "assets", assets)
if not self.manifest_id:
object.__setattr__(
self,
"manifest_id",
_stable_id(
"asset-manifest",
self.source_path,
self.source_digest,
[asset.asset_id for asset in assets],
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"manifest_id": self.manifest_id,
"source_path": self.source_path,
"source_digest": self.source_digest,
"assets": [asset.to_dict() for asset in self.assets],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any] | None) -> "RenderAssetManifest":
if not data:
return cls()
return cls(
manifest_id=str(data.get("manifest_id", "")),
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_digest=str(data["source_digest"]) if data.get("source_digest") is not None else None,
assets=[RenderAsset.from_dict(asset) for asset in data.get("assets", [])],
schema_version=str(data.get("schema_version", RENDER_REFERENCE_SCHEMA_VERSION)),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderSourceMap:
"""Trace from Markitect source/generated output to renderer units or artifacts."""
map_id: str = ""
source: RenderSourceSpan | None = None
source_unit_id: str | None = None
generated_by: str | None = None
function_run_id: str | None = None
render_unit_id: str | None = None
artifact_id: str | None = None
artifact_ref: str | None = None
role: str | None = None
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if not (self.render_unit_id or self.artifact_id or self.artifact_ref):
raise RenderReferenceError(
"Render source map needs render_unit_id, artifact_id, or artifact_ref"
)
if not self.map_id:
object.__setattr__(
self,
"map_id",
_stable_id(
"source-map",
self.source.to_dict() if self.source else None,
self.source_unit_id,
self.generated_by,
self.function_run_id,
self.render_unit_id,
self.artifact_id,
self.artifact_ref,
self.role,
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"map_id": self.map_id,
"source": self.source.to_dict() if self.source else None,
"source_unit_id": self.source_unit_id,
"generated_by": self.generated_by,
"function_run_id": self.function_run_id,
"render_unit_id": self.render_unit_id,
"artifact_id": self.artifact_id,
"artifact_ref": self.artifact_ref,
"role": self.role,
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderSourceMap":
return cls(
map_id=str(data.get("map_id", "")),
source=RenderSourceSpan.from_dict(data.get("source")),
source_unit_id=str(data["source_unit_id"]) if data.get("source_unit_id") is not None else None,
generated_by=str(data["generated_by"]) if data.get("generated_by") is not None else None,
function_run_id=str(data["function_run_id"]) if data.get("function_run_id") is not None else None,
render_unit_id=str(data["render_unit_id"]) if data.get("render_unit_id") is not None else None,
artifact_id=str(data["artifact_id"]) if data.get("artifact_id") is not None else None,
artifact_ref=str(data["artifact_ref"]) if data.get("artifact_ref") is not None else None,
role=str(data["role"]) if data.get("role") is not None else None,
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderReferenceManifest:
"""Passive render structure, cross-reference, asset, and provenance manifest."""
manifest_id: str = ""
units: list[RenderUnitReference] = field(default_factory=list)
cross_references: list[RenderCrossReference] = field(default_factory=list)
toc: list[RenderTocEntry] = field(default_factory=list)
asset_manifest: RenderAssetManifest = field(default_factory=RenderAssetManifest)
source_maps: list[RenderSourceMap] = field(default_factory=list)
source_path: str | None = None
source_digest: str | None = None
schema_version: str = RENDER_REFERENCE_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
def __post_init__(self) -> None:
if self.schema_version != RENDER_REFERENCE_SCHEMA_VERSION:
raise RenderReferenceError(f"Expected schema_version `{RENDER_REFERENCE_SCHEMA_VERSION}`")
_check_duplicates([unit.unit_id for unit in self.units], "render unit")
_check_duplicates([entry.entry_id for entry in self.toc], "TOC entry")
_check_duplicates([event.map_id for event in self.source_maps], "source map")
unit_ids = {unit.unit_id for unit in self.units}
if unit_ids:
for cross_reference in self.cross_references:
if cross_reference.target_unit_id not in unit_ids:
raise RenderReferenceError(
f"Cross-reference `{cross_reference.reference_id}` targets unknown unit "
f"`{cross_reference.target_unit_id}`"
)
for entry in self.toc:
if entry.unit_id not in unit_ids:
raise RenderReferenceError(
f"TOC entry `{entry.entry_id}` targets unknown unit `{entry.unit_id}`"
)
if not self.manifest_id:
object.__setattr__(
self,
"manifest_id",
_stable_id(
"render-manifest",
self.source_path,
self.source_digest,
[unit.unit_id for unit in self.units],
[reference.reference_id for reference in self.cross_references],
[entry.entry_id for entry in self.toc],
self.asset_manifest.manifest_id,
[event.map_id for event in self.source_maps],
),
)
def to_dict(self) -> dict[str, Any]:
return _drop_empty(
{
"schema_version": self.schema_version,
"kind": RENDER_REFERENCE_MANIFEST_KIND,
"manifest_id": self.manifest_id,
"source_path": self.source_path,
"source_digest": self.source_digest,
"units": [unit.to_dict() for unit in self.units],
"cross_references": [reference.to_dict() for reference in self.cross_references],
"toc": [entry.to_dict() for entry in self.toc],
"asset_manifest": self.asset_manifest.to_dict(),
"source_maps": [event.to_dict() for event in self.source_maps],
"metadata": self.metadata,
}
)
@classmethod
def from_dict(cls, data: dict[str, Any]) -> "RenderReferenceManifest":
return cls(
manifest_id=str(data.get("manifest_id", "")),
units=[RenderUnitReference.from_dict(unit) for unit in data.get("units", [])],
cross_references=[
RenderCrossReference.from_dict(reference)
for reference in data.get("cross_references", [])
],
toc=[RenderTocEntry.from_dict(entry) for entry in data.get("toc", [])],
asset_manifest=RenderAssetManifest.from_dict(data.get("asset_manifest")),
source_maps=[RenderSourceMap.from_dict(event) for event in data.get("source_maps", [])],
source_path=str(data["source_path"]) if data.get("source_path") is not None else None,
source_digest=str(data["source_digest"]) if data.get("source_digest") is not None else None,
schema_version=str(data.get("schema_version", RENDER_REFERENCE_SCHEMA_VERSION)),
metadata=dict(data.get("metadata", {})),
)
@dataclass(frozen=True)
class RenderArtifact:
"""Metadata for a rendered or exported artifact."""
@@ -115,6 +786,7 @@ class RenderExportRequest:
source_path: str | None = None
options: dict[str, Any] = field(default_factory=dict)
policy: dict[str, Any] = field(default_factory=dict)
render_manifest: RenderReferenceManifest | dict[str, Any] | None = None
schema_version: str = RENDER_EXPORT_SCHEMA_VERSION
metadata: dict[str, Any] = field(default_factory=dict)
@@ -128,6 +800,7 @@ class RenderExportRequest:
"source_path": self.source_path,
"options": self.options,
"policy": self.policy,
"render_manifest": _render_manifest_dict(self.render_manifest),
"metadata": self.metadata,
}
)
@@ -384,18 +1057,20 @@ class FakeRenderExportAdapter:
operation=request.operation,
profile=request.profile,
provenance=provenance,
metadata={
"profile": request.profile,
"supported_operations": self.descriptor.operations,
"external_renderer_invoked": False,
},
metadata=_fake_result_metadata(
request,
{
"profile": request.profile,
"supported_operations": self.descriptor.operations,
},
),
)
if request.operation == "export-source":
artifact = RenderArtifact.from_content(
exported,
role="renderer-source",
media_type="text/markdown",
metadata={"profile": request.profile},
metadata=_fake_artifact_metadata(request, {"profile": request.profile}),
)
provenance = [
_event_with_artifact(event, artifact.artifact_id)
@@ -408,14 +1083,14 @@ class FakeRenderExportAdapter:
artifacts=[artifact],
exported_source=exported,
provenance=provenance,
metadata={"external_renderer_invoked": False},
metadata=_fake_result_metadata(request),
)
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},
metadata=_fake_artifact_metadata(request, {"profile": request.profile, "fake_renderer": True}),
)
provenance = [_event_with_artifact(event, artifact.artifact_id) for event in provenance]
return RenderExportResult(
@@ -425,7 +1100,7 @@ class FakeRenderExportAdapter:
artifacts=[artifact],
exported_source=exported,
provenance=provenance,
metadata={"external_renderer_invoked": False},
metadata=_fake_result_metadata(request),
)
@@ -527,6 +1202,45 @@ def render_export_registry_descriptor() -> ExtensionDescriptor:
)
def render_reference_manifest_descriptor() -> ExtensionDescriptor:
"""Descriptor for passive render reference and asset manifest contracts."""
return ExtensionDescriptor(
id="render.reference-manifest",
kind="render-reference-contract",
summary="Passive render unit, cross-reference, TOC, asset manifest, and source-map contracts.",
capabilities=[
ProcessingCapability(id="render_references", kind="model"),
ProcessingCapability(id="asset_manifest", kind="model"),
ProcessingCapability(id="source_maps", kind="model"),
ProcessingCapability(id="provenance", kind="emit"),
],
safety={
"filesystem_read": False,
"filesystem_write": False,
"external_process": False,
"network": False,
"asset_copying": False,
"final_numbering": False,
},
input_contract="Markdown units | normalized source attachments | renderer source planning metadata",
output_contract="RenderReferenceManifest",
diagnostics_namespace="render.reference",
provenance_prefix="render.reference_manifest",
docs=["docs/render-reference-asset-manifest.md"],
examples=["examples/render/render-reference-manifest.yaml"],
metadata={
"schema_version": RENDER_REFERENCE_SCHEMA_VERSION,
"kind": RENDER_REFERENCE_MANIFEST_KIND,
"unit_kinds": sorted(RENDER_UNIT_KINDS),
"copy_policies": sorted(RENDER_ASSET_COPY_POLICIES),
"core_performs_asset_copying": False,
"core_assigns_final_numbering": False,
"markitect_filter_boundary": "read-side source asset and attachment metadata only",
},
)
def fake_render_export_adapter_descriptor() -> RenderExportAdapterDescriptor:
"""Descriptor for the built-in fake render/export adapter."""
@@ -603,6 +1317,45 @@ def _render_export_capabilities() -> list[ProcessingCapability]:
]
def render_unit_id(
kind: str,
*,
source_path: str | None = None,
anchor: str | None = None,
content_hash: str | None = None,
ordinal_hint: int | None = None,
title: str | None = None,
) -> str:
"""Return a deterministic render unit id for passive reference manifests."""
if kind not in RENDER_UNIT_KINDS:
raise RenderReferenceError(f"Unsupported render unit kind `{kind}`")
return _stable_id("render-unit", kind, source_path, anchor, content_hash, ordinal_hint, title)
def render_asset_id(
source: str,
*,
digest: str | None = None,
role: str | None = None,
output_reference: str | None = None,
) -> str:
"""Return a deterministic asset id without copying or reading the asset."""
if not source.strip():
raise RenderReferenceError("Render asset id source cannot be empty")
return _stable_id("asset", source, digest, role, output_reference)
def render_manifest_id(manifest: RenderReferenceManifest | dict[str, Any]) -> str:
"""Return the deterministic id for a render reference manifest-like object."""
data = _render_manifest_dict(manifest)
if data and data.get("manifest_id"):
return str(data["manifest_id"])
return _stable_id("render-manifest", data)
def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderProvenance:
return RenderProvenance(
operation=event.operation,
@@ -615,6 +1368,57 @@ def _event_with_artifact(event: RenderProvenance, artifact_id: str) -> RenderPro
)
def _fake_result_metadata(
request: RenderExportRequest,
base: dict[str, Any] | None = None,
) -> dict[str, Any]:
metadata = {"external_renderer_invoked": False}
metadata.update(base or {})
metadata.update(_manifest_summary_metadata(request.render_manifest))
return metadata
def _fake_artifact_metadata(
request: RenderExportRequest,
base: dict[str, Any] | None = None,
) -> dict[str, Any]:
metadata = dict(base or {})
metadata.update(_manifest_summary_metadata(request.render_manifest))
return metadata
def _manifest_summary_metadata(manifest: RenderReferenceManifest | dict[str, Any] | None) -> dict[str, Any]:
data = _render_manifest_dict(manifest)
if not data:
return {}
asset_manifest = data.get("asset_manifest", {})
assets = asset_manifest.get("assets", []) if isinstance(asset_manifest, dict) else []
return _drop_empty(
{
"render_reference_manifest_id": data.get("manifest_id"),
"render_reference_schema_version": data.get("schema_version"),
"render_units": len(data.get("units", [])),
"render_cross_references": len(data.get("cross_references", [])),
"render_toc_entries": len(data.get("toc", [])),
"render_source_maps": len(data.get("source_maps", [])),
"asset_manifest_id": asset_manifest.get("manifest_id")
if isinstance(asset_manifest, dict)
else None,
"render_assets": len(assets),
}
)
def _render_manifest_dict(manifest: RenderReferenceManifest | dict[str, Any] | None) -> dict[str, Any]:
if manifest is None:
return {}
if isinstance(manifest, RenderReferenceManifest):
return manifest.to_dict()
if isinstance(manifest, dict):
return dict(manifest)
raise RenderReferenceError("render_manifest must be a RenderReferenceManifest or mapping")
def _fake_exported_source(request: RenderExportRequest) -> str:
return (
f"<!-- render.fake profile={request.profile} operation={request.operation} -->\n\n"
@@ -630,6 +1434,44 @@ def _fake_media_type(profile: str) -> str:
return "text/plain"
def _stable_id(prefix: str, *parts: Any) -> str:
payload = json.dumps(parts, sort_keys=True, separators=(",", ":"), default=str)
digest = hashlib.sha256(payload.encode("utf-8")).hexdigest()[:16]
return f"{prefix}:{digest}"
def _check_duplicates(values: list[str], label: str) -> None:
seen: set[str] = set()
duplicates: set[str] = set()
for value in values:
if value in seen:
duplicates.add(value)
seen.add(value)
if duplicates:
raise RenderReferenceError(
f"Duplicate {label} id(s): " + ", ".join(sorted(duplicates))
)
def _mapping_from(value: Any) -> dict[str, Any]:
if isinstance(value, dict):
return dict(value)
to_dict = getattr(value, "to_dict", None)
if callable(to_dict):
return dict(to_dict())
raise RenderReferenceError("Expected a mapping or object with to_dict()")
def _render_kind_from_content_unit(kind: str) -> str:
if kind == "heading":
return "section"
if kind == "code":
return "code-block"
if kind in RENDER_UNIT_KINDS:
return kind
return "custom"
def _digest_text(value: str) -> str:
return "sha256:" + hashlib.sha256(value.encode("utf-8")).hexdigest()