Complete independent projection runtime
This commit is contained in:
parent
1134c2d375
commit
f1fabaf0ca
38 changed files with 4907 additions and 87 deletions
|
|
@ -26,6 +26,8 @@ from .models import (
|
|||
RenderView,
|
||||
)
|
||||
from .project import project_root_fingerprint
|
||||
from .projection_contract import ProjectionReceiptV1
|
||||
from .projection_policy import ManualProjectionMode, validate_manual_projection_mode
|
||||
from .render_contract import PreparedRender, relative_output, renderer_for
|
||||
from .telemetry import increment, stage
|
||||
|
||||
|
|
@ -36,9 +38,26 @@ MAX_RENDER_RECEIPT_BYTES = 64_000
|
|||
class RenderService:
|
||||
"""Render only declared views through fixed built-in renderer implementations."""
|
||||
|
||||
def __init__(self, project: ProjectService, changesets: ChangesetStore | None = None) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
project: ProjectService,
|
||||
changesets: ChangesetStore | None = None,
|
||||
*,
|
||||
manual_policy: ManualProjectionMode = "explicit",
|
||||
) -> None:
|
||||
self.project = project
|
||||
self.changesets = changesets or ChangesetStore(project)
|
||||
self.manual_policy = validate_manual_projection_mode(manual_policy)
|
||||
|
||||
def _require_rendering(self, operation: str) -> None:
|
||||
if self.manual_policy == "disabled":
|
||||
raise DocForgeError(
|
||||
"projection_policy_forbids_operation",
|
||||
"Manual projection policy disables rendering work",
|
||||
projection="manual",
|
||||
mode=self.manual_policy,
|
||||
operation=operation,
|
||||
)
|
||||
|
||||
def status(self, view_id: str | None = None) -> dict[str, object]:
|
||||
"""Report publication state from bounded receipts without rendering canonical content."""
|
||||
|
|
@ -86,6 +105,7 @@ class RenderService:
|
|||
def deep_status(self, view_id: str | None = None) -> dict[str, object]:
|
||||
"""Recompute render output as the explicit side-effect-free equivalence oracle."""
|
||||
|
||||
self._require_rendering("deep_status")
|
||||
snapshot = self.project.load()
|
||||
config = snapshot.descriptor.render
|
||||
if config is None:
|
||||
|
|
@ -107,7 +127,12 @@ class RenderService:
|
|||
snapshot.descriptor.root,
|
||||
view.output_path,
|
||||
)
|
||||
prepared, _ = self._prepare(snapshot, view, changeset_hash=None)
|
||||
prepared, _ = self._prepare(
|
||||
snapshot,
|
||||
view,
|
||||
changeset_hash=None,
|
||||
incremental=False,
|
||||
)
|
||||
state = "missing"
|
||||
actual_hash: str | None = None
|
||||
output = view.output_path
|
||||
|
|
@ -151,6 +176,7 @@ class RenderService:
|
|||
)
|
||||
|
||||
def render(self, view_id: str) -> dict[str, object]:
|
||||
self._require_rendering("render")
|
||||
with self._lock():
|
||||
snapshot = self.project.load()
|
||||
config = self._config(snapshot)
|
||||
|
|
@ -373,6 +399,8 @@ class RenderService:
|
|||
"template_file": final_template_file,
|
||||
"output_file": final_output_file,
|
||||
}
|
||||
if prepared.projection_receipt is not None:
|
||||
payload["projection_receipt"] = prepared.projection_receipt
|
||||
raw = json.dumps(payload, sort_keys=True, indent=2).encode("utf-8") + b"\n"
|
||||
if len(raw) > MAX_RENDER_RECEIPT_BYTES:
|
||||
raise DocForgeError(
|
||||
|
|
@ -572,8 +600,10 @@ class RenderService:
|
|||
renderer = renderer_for(view)
|
||||
template_file = receipt.get("template_file")
|
||||
output_file = receipt.get("output_file")
|
||||
fields = set(receipt)
|
||||
projection_receipt = receipt.get("projection_receipt")
|
||||
return (
|
||||
set(receipt) == required
|
||||
fields in (required, required | {"projection_receipt"})
|
||||
and receipt.get("schema_version") == RENDER_RECEIPT_SCHEMA_VERSION
|
||||
and receipt.get("project_id") == descriptor.project_id
|
||||
and receipt.get("project_root_fingerprint") == project_root_fingerprint(descriptor.root)
|
||||
|
|
@ -602,6 +632,50 @@ class RenderService:
|
|||
descriptor.limits.max_render_bytes,
|
||||
)
|
||||
and cast(dict[str, object], output_file)["size"] == receipt.get("output_bytes")
|
||||
and (
|
||||
projection_receipt is None
|
||||
or self._valid_projection_receipt(
|
||||
projection_receipt,
|
||||
renderer_id=renderer.renderer_id,
|
||||
renderer_version=renderer.renderer_version,
|
||||
output_hash=cast(str, receipt["output_hash"]),
|
||||
output_bytes=cast(int, receipt["output_bytes"]),
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _valid_projection_receipt(
|
||||
value: object,
|
||||
*,
|
||||
renderer_id: str,
|
||||
renderer_version: str,
|
||||
output_hash: str,
|
||||
output_bytes: int,
|
||||
) -> bool:
|
||||
if not isinstance(value, dict):
|
||||
return False
|
||||
try:
|
||||
receipt = ProjectionReceiptV1.from_dict(cast(dict[str, object], value))
|
||||
except DocForgeError:
|
||||
return False
|
||||
document = receipt.document
|
||||
return (
|
||||
document.get("kind") == "manual"
|
||||
and document.get("renderer")
|
||||
== {
|
||||
"renderer_id": renderer_id,
|
||||
"renderer_version": renderer_version,
|
||||
}
|
||||
and document.get("artifacts")
|
||||
== [
|
||||
{
|
||||
"artifact_id": "manual.html",
|
||||
"media_type": "text/html; charset=utf-8",
|
||||
"sha256": output_hash,
|
||||
"bytes": output_bytes,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -663,6 +737,7 @@ class RenderService:
|
|||
"reason": reason,
|
||||
"verification": "receipt",
|
||||
"receipt_schema_version": payload.get("schema_version"),
|
||||
"projection_receipt": payload.get("projection_receipt"),
|
||||
}
|
||||
|
||||
def _current_state(self) -> ProjectState | None:
|
||||
|
|
@ -687,6 +762,7 @@ class RenderService:
|
|||
}
|
||||
|
||||
def preview(self, changeset_id: str, view_id: str) -> dict[str, object]:
|
||||
self._require_rendering("preview")
|
||||
with self._lock():
|
||||
snapshot, changeset_hash = self.changesets.projected_snapshot(changeset_id)
|
||||
config = self._config(snapshot)
|
||||
|
|
@ -731,11 +807,12 @@ class RenderService:
|
|||
view: RenderView,
|
||||
*,
|
||||
changeset_hash: str | None,
|
||||
incremental: bool = True,
|
||||
) -> tuple[PreparedRender, bytes]:
|
||||
increment("render_prepare_calls")
|
||||
template = self._template_bytes(snapshot, view)
|
||||
with stage("render.prepare"):
|
||||
prepared = renderer_for(view).prepare(
|
||||
prepared = renderer_for(view, incremental=incremental).prepare(
|
||||
snapshot,
|
||||
view,
|
||||
template,
|
||||
|
|
@ -850,6 +927,7 @@ class RenderService:
|
|||
"expected_output_hash": prepared.output_hash,
|
||||
"actual_output_hash": actual_hash,
|
||||
"template_hash": prepared.template_hash,
|
||||
"projection_receipt": prepared.projection_receipt,
|
||||
"path": relative_output(snapshot, view.output_path),
|
||||
"state": state,
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue