1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages
DocForge2/src/docforge/render_contract.py

146 lines
4.6 KiB
Python
Raw Normal View History

"""Compatibility shim over the versioned manual projection boundary."""
from __future__ import annotations
import hashlib
import json
from dataclasses import dataclass
from importlib.metadata import version
from pathlib import Path
from typing import Protocol
from .errors import DocForgeError
from .manual_projection import build_manual_projection_package, build_manual_render_plan
from .models import ProjectSnapshot, RenderView
@dataclass(frozen=True)
class PreparedRender:
render_identity: str
output_hash: str
output: bytes
renderer: str
renderer_version: str
template_hash: str
class Renderer(Protocol):
"""Fixed interface implemented by explicitly registered built-in renderers."""
renderer_id: str
renderer_version: str
def prepare(
self,
snapshot: ProjectSnapshot,
view: RenderView,
template_bytes: bytes,
*,
changeset_hash: str | None,
) -> PreparedRender: ...
class GenericHtmlRenderer:
"""Preserve the public v1 renderer API over the plan-only manual renderer."""
renderer_id = "generic_html"
contract_version = "1"
def __init__(self) -> None:
self.renderer_version = (
f"{self.contract_version}+markdown-it-py-{version('markdown-it-py')}"
)
def prepare(
self,
snapshot: ProjectSnapshot,
view: RenderView,
template_bytes: bytes,
*,
changeset_hash: str | None,
) -> PreparedRender:
selected = tuple(
node for node in snapshot.nodes if not view.families or node.family in view.families
)
selected_ids = {node.node_id for node in selected}
selected_edges = tuple(
edge
for edge in snapshot.edges
if edge.source_id in selected_ids and edge.target_id in selected_ids
)
template_hash = hashlib.sha256(template_bytes).hexdigest()
identity_payload = {
"schema_version": 1,
"project_id": snapshot.descriptor.project_id,
"adapter": snapshot.descriptor.adapter,
"source_hash": snapshot.source_hash,
"changeset_hash": changeset_hash,
"renderer": self.renderer_id,
"renderer_version": self.renderer_version,
"view": {
"id": view.view_id,
"title": view.title,
"families": list(view.families),
"output": view.output_path.relative_to(snapshot.descriptor.root).as_posix(),
},
"template_hash": template_hash,
"nodes": [
{
"id": node.node_id,
"content_hash": node.content_hash,
"source_path": node.source_path,
}
for node in selected
],
"edges": [edge.as_dict() for edge in selected_edges],
}
render_identity = hashlib.sha256(
json.dumps(identity_payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
).hexdigest()
plan = build_manual_render_plan(snapshot, view, changeset_hash=changeset_hash)
package = build_manual_projection_package(
plan,
template_bytes,
renderer_id=self.renderer_id,
renderer_version=self.renderer_version,
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
)
from docforge_renderers.manual import ManualHtmlRenderer
result = ManualHtmlRenderer(self.renderer_version).render(
package,
render_identity=render_identity,
)
if len(result.artifacts) != 1:
raise DocForgeError(
"invalid_projection",
"Manual renderer returned an unsupported artifact set",
)
output = result.artifacts[0].content
return PreparedRender(
render_identity=render_identity,
output_hash=hashlib.sha256(output).hexdigest(),
output=output,
renderer=self.renderer_id,
renderer_version=self.renderer_version,
template_hash=template_hash,
)
_RENDERERS: dict[str, type[GenericHtmlRenderer]] = {
GenericHtmlRenderer.renderer_id: GenericHtmlRenderer
}
def renderer_for(view: RenderView) -> Renderer:
factory = _RENDERERS.get(view.renderer)
if factory is None:
raise DocForgeError(
"unsupported_renderer", "View does not name a supported built-in renderer"
)
return factory()
def relative_output(snapshot: ProjectSnapshot, path: Path) -> str:
return path.relative_to(snapshot.descriptor.root).as_posix()