generated from coulomb/repo-seed
Add render reference asset manifest contract
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user