1
0
Fork 0
Code Issues Pull requests Projects Releases 2 Packages Wiki Activity Actions Pages

Complete independent projection runtime

This commit is contained in:
Andraxion 2026-07-29 12:38:25 -04:00
parent 1134c2d375
commit f1fabaf0ca
38 changed files with 4907 additions and 87 deletions

View file

@ -3,15 +3,28 @@
from __future__ import annotations
import hashlib
import html
import json
from dataclasses import dataclass
from importlib.metadata import version
from pathlib import Path
from typing import Protocol
from typing import Protocol, cast
from .errors import DocForgeError
from .manual_projection import build_manual_projection_package, build_manual_render_plan
from .models import ProjectSnapshot, RenderView
from .projection_contract import (
ManualRenderPlanV1,
ProjectionPackageV1,
ProjectionRenderResult,
)
from .projection_fragments import (
FragmentKey,
FragmentRecord,
ProjectionFragmentCache,
fragment_semantic_hash,
)
from .projection_worker import render_projection_in_worker
@dataclass(frozen=True)
@ -22,6 +35,7 @@ class PreparedRender:
renderer: str
renderer_version: str
template_hash: str
projection_receipt: dict[str, object] | None = None
class Renderer(Protocol):
@ -46,10 +60,13 @@ class GenericHtmlRenderer:
renderer_id = "generic_html"
contract_version = "1"
def __init__(self) -> None:
page_component_version = "manual.page@1"
def __init__(self, *, incremental: bool = True) -> None:
self.renderer_version = (
f"{self.contract_version}+markdown-it-py-{version('markdown-it-py')}"
)
self.incremental = incremental
def prepare(
self,
@ -98,19 +115,24 @@ class GenericHtmlRenderer:
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(
full_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 self.incremental:
result = self._incremental_result(
snapshot,
plan,
template_bytes,
full_package=full_package,
render_identity=render_identity,
)
else:
result = render_projection_in_worker(full_package)
if len(result.artifacts) != 1:
raise DocForgeError(
"invalid_projection",
@ -124,21 +146,128 @@ class GenericHtmlRenderer:
renderer=self.renderer_id,
renderer_version=self.renderer_version,
template_hash=template_hash,
projection_receipt=result.receipt.as_dict(),
)
def _incremental_result(
self,
snapshot: ProjectSnapshot,
plan: ManualRenderPlanV1,
template_bytes: bytes,
*,
full_package: ProjectionPackageV1,
render_identity: str,
) -> ProjectionRenderResult:
cache = ProjectionFragmentCache(
snapshot.descriptor.root,
snapshot.descriptor.cache_root,
)
pages = cast(list[object], plan.document["pages"])
records: list[FragmentRecord | None] = []
missing: list[tuple[int, FragmentKey]] = []
for value in pages:
page = cast(dict[str, object], value)
key = FragmentKey.create(
projection_kind="manual",
renderer_id=self.renderer_id,
renderer_version=self.renderer_version,
component_version=self.page_component_version,
semantic_input_hash=fragment_semantic_hash(page),
)
record = cache.get(key)
if record is None:
missing.append((len(records), key))
records.append(record)
if not records:
return render_projection_in_worker(full_package)
full_result: ProjectionRenderResult | None = None
new_records: list[FragmentRecord] = []
if missing:
full_result = render_projection_in_worker(full_package)
fragments = self._extract_page_fragments(
pages,
full_result.artifacts[0].content,
)
try:
for position, key in missing:
record = FragmentRecord.create(key, fragments[position])
records[position] = record
new_records.append(record)
except DocForgeError:
return full_result
complete_records = [record for record in records if record is not None]
if len(complete_records) != len(records):
return full_result or render_projection_in_worker(full_package)
try:
incremental_package = build_manual_projection_package(
plan,
template_bytes,
fragment_records=[record.as_dict() for record in complete_records],
renderer_id=self.renderer_id,
renderer_version=self.renderer_version,
max_output_bytes=snapshot.descriptor.limits.max_render_bytes,
render_identity=render_identity,
)
incremental_result = render_projection_in_worker(incremental_package)
except DocForgeError:
cache.prune(())
return full_result or render_projection_in_worker(full_package)
if full_result is None:
cache.prune(tuple(record.key for record in complete_records))
return incremental_result
if tuple(
(artifact.artifact_id, artifact.media_type, artifact.content)
for artifact in incremental_result.artifacts
) != tuple(
(artifact.artifact_id, artifact.media_type, artifact.content)
for artifact in full_result.artifacts
):
cache.prune(())
return full_result
for record in new_records:
cache.put(record.key, record.content)
cache.prune(tuple(record.key for record in complete_records))
return incremental_result
@staticmethod
def _extract_page_fragments(
pages: list[object],
output: bytes,
) -> list[bytes]:
"""Extract exact deterministic page sections from one trusted full artifact."""
fragments: list[bytes] = []
cursor = 0
closing = b"</section>"
for value in pages:
page = cast(dict[str, object], value)
node_id = cast(str, page["node_id"])
marker = f'<section id="node-{html.escape(node_id, quote=True)}">'.encode()
start = output.find(marker, cursor)
end = output.find(closing, start + len(marker)) if start >= 0 else -1
if start < 0 or end < 0:
raise DocForgeError(
"invalid_projection",
"Full manual artifact does not contain its planned page fragments",
)
end += len(closing)
fragments.append(output[start:end])
cursor = end
return fragments
_RENDERERS: dict[str, type[GenericHtmlRenderer]] = {
GenericHtmlRenderer.renderer_id: GenericHtmlRenderer
}
def renderer_for(view: RenderView) -> Renderer:
def renderer_for(view: RenderView, *, incremental: bool = True) -> 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()
return factory(incremental=incremental)
def relative_output(snapshot: ProjectSnapshot, path: Path) -> str: