"""base.py — the SurfaceProjector interface + registry. THE key abstraction of the efferent projector: a projector is a pure function from the surface-neutral, geometry-carrying DocumentIR to bytes on a target SURFACE. The surface is a PARAMETER. Adding a surface = registering one more projector; nothing upstream (plan/realize/cohere) changes. DocumentIR --project--> bytes (per surface) A TEXT projector reads ``block.sentences``. A NON-TEXT projector (music, image, video) reads ``block.provenance`` — the geometry the IR carries — and decodes it onto its surface. Both consume the SAME IR. That symmetry is the whole design: the realizer generalizes into a multimodal projector, geometry -> any surface. """ from __future__ import annotations from typing import Protocol, runtime_checkable import sys import os sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from document_ir import DocumentIR # noqa: E402 @runtime_checkable class SurfaceProjector(Protocol): """Geometry-document -> one surface. Implementations MUST be pure & faithful. THE ONE SHARED SEAM. Every surface — text, music, image, video — conforms to this single contract: project(frame: DocumentIR) -> bytes where ``frame`` is the geometry-carrying meaning-geometry (the SemFrame at document scale; a single utterance is the degenerate one-section frame). RECOMMENDED INTERNAL SHAPE (the peer music/text decomposition, blessed here so all surfaces share it): a projector may split ``project`` into spec = self.plan(frame) # meaning-geometry -> surface-specific spec bytes = self.realize(spec) # spec -> surface, via this projector's PROFILE ``project`` is then ``realize(plan(frame))``. The PROFILE (a text lang-profile, a music instr/mode-profile, an image layout-profile) is a property of the projector instance — the pluggable knob. See :class:`TwoStageProjector`. A TEXT projector's plan reads ``frame`` sentences; a MUSIC/IMAGE projector's plan reads ``frame.all_provenance()`` — the geometry — and derives its spec (pitch/harmony/rhythm, or layout) FROM the meaning, deterministically. Same frame, different profile. """ surface: str # "markdown" | "docx" | "midi" | "audio" | "image" | "video" media_type: str # MIME type of the emitted bytes ext: str # file extension (no dot) modality: str # "text" | "audio" | "image" | "video" profile: object # the pluggable per-surface profile (may be None) def project(self, doc: DocumentIR) -> bytes: """Emit the document on this surface. Returns raw bytes.""" ... class TwoStageProjector: """Optional base for the peer plan()/realize() decomposition. Subclasses implement ``plan(frame) -> spec`` and ``realize(spec) -> bytes``; ``project`` is their composition. This is exactly the peer music interface (spec = plan(frame, profile); surface = realize(spec, profile)) expressed so that it still satisfies the single ``SurfaceProjector.project`` seam. Text, music, and image projectors can all subclass this and remain interchangeable. """ surface: str = "" media_type: str = "" ext: str = "" modality: str = "" profile: object = None def plan(self, doc: DocumentIR): # -> spec raise NotImplementedError def realize(self, spec) -> bytes: raise NotImplementedError def project(self, doc: DocumentIR) -> bytes: return self.realize(self.plan(doc)) _REGISTRY: dict[str, SurfaceProjector] = {} def register(projector: SurfaceProjector) -> SurfaceProjector: _REGISTRY[projector.surface] = projector return projector def get_projector(surface: str) -> SurfaceProjector: if surface not in _REGISTRY: raise KeyError(f"no projector registered for surface {surface!r}; " f"have {sorted(_REGISTRY)}") return _REGISTRY[surface] def available_surfaces() -> list[str]: return sorted(_REGISTRY)