"""Independent version-2 policy for manual, portable graph, and live projections.""" from __future__ import annotations import hashlib import json from dataclasses import dataclass from typing import Literal, cast from .errors import DocForgeError ManualProjectionMode = Literal["auto", "explicit", "disabled"] PortableGraphProjectionMode = Literal["explicit", "disabled"] LiveViewerProjectionMode = Literal["on-demand", "disabled"] MANUAL_PROJECTION_MODES: tuple[ManualProjectionMode, ...] = ( "auto", "explicit", "disabled", ) PORTABLE_GRAPH_PROJECTION_MODES: tuple[PortableGraphProjectionMode, ...] = ( "explicit", "disabled", ) LIVE_VIEWER_PROJECTION_MODES: tuple[LiveViewerProjectionMode, ...] = ( "on-demand", "disabled", ) def _invalid_mode(field: str, value: object, allowed: tuple[str, ...]) -> DocForgeError: return DocForgeError( "invalid_projection_policy", "Projection policy mode is unsupported", projection=field, mode=value, allowed=list(allowed), ) def _select_mode( value: object | None, *, field: str, default: str, allowed: tuple[str, ...], ) -> str: selected: object = default if value is None else value if not isinstance(selected, str) or selected not in allowed: raise _invalid_mode(field, selected, allowed) return selected def _require_boolean(field: str, value: object) -> bool: if type(value) is not bool: raise DocForgeError( "invalid_projection_policy", "Projection policy availability must be Boolean", field=field, ) return value def _unavailable(field: str, mode: str, required: str) -> DocForgeError: return DocForgeError( "projection_policy_unavailable", "Projection policy mode is unavailable", projection=field, mode=mode, required=required, ) def validate_manual_projection_mode(value: object) -> ManualProjectionMode: """Validate one direct manual-service policy selection.""" return cast( ManualProjectionMode, _select_mode( value, field="manual", default="explicit", allowed=MANUAL_PROJECTION_MODES, ), ) def validate_portable_graph_projection_mode( value: object, ) -> PortableGraphProjectionMode: """Validate one direct portable-graph service policy selection.""" return cast( PortableGraphProjectionMode, _select_mode( value, field="portable_graph", default="explicit", allowed=PORTABLE_GRAPH_PROJECTION_MODES, ), ) def validate_live_viewer_projection_mode(value: object) -> LiveViewerProjectionMode: """Validate one direct live-viewer service policy selection.""" return cast( LiveViewerProjectionMode, _select_mode( value, field="live_viewer", default="on-demand", allowed=LIVE_VIEWER_PROJECTION_MODES, ), ) @dataclass(frozen=True) class ProjectionPolicyV2: """One immutable policy for three independent projection consumers.""" manual: ManualProjectionMode portable_graph: PortableGraphProjectionMode live_viewer: LiveViewerProjectionMode def __post_init__(self) -> None: if self.manual not in MANUAL_PROJECTION_MODES: raise _invalid_mode("manual", self.manual, MANUAL_PROJECTION_MODES) if self.portable_graph not in PORTABLE_GRAPH_PROJECTION_MODES: raise _invalid_mode( "portable_graph", self.portable_graph, PORTABLE_GRAPH_PROJECTION_MODES, ) if self.live_viewer not in LIVE_VIEWER_PROJECTION_MODES: raise _invalid_mode( "live_viewer", self.live_viewer, LIVE_VIEWER_PROJECTION_MODES, ) def as_dict(self) -> dict[str, object]: return { "schema_version": 2, "manual": self.manual, "portable_graph": self.portable_graph, "live_viewer": self.live_viewer, } @property def policy_hash(self) -> str: raw = json.dumps( self.as_dict(), sort_keys=True, separators=(",", ":"), ensure_ascii=False, ).encode("utf-8") return hashlib.sha256(raw).hexdigest() def compose_projection_policy( *, manual: str | None = None, portable_graph: str | None = None, live_viewer: str | None = None, manual_configured: bool, portable_graph_configured: bool, application_enabled: bool, live_viewer_available: bool = True, ) -> ProjectionPolicyV2: """Compose compatible defaults with explicit availability-checked selections.""" manual_available = _require_boolean("manual_configured", manual_configured) graph_available = _require_boolean( "portable_graph_configured", portable_graph_configured, ) application_available = _require_boolean("application_enabled", application_enabled) viewer_available = _require_boolean("live_viewer_available", live_viewer_available) default_manual = ( "auto" if manual_available and application_available else ("explicit" if manual_available else "disabled") ) default_graph = "explicit" if graph_available else "disabled" default_viewer = "on-demand" if viewer_available else "disabled" manual_mode = cast( ManualProjectionMode, _select_mode( manual, field="manual", default=default_manual, allowed=MANUAL_PROJECTION_MODES, ), ) graph_mode = cast( PortableGraphProjectionMode, _select_mode( portable_graph, field="portable_graph", default=default_graph, allowed=PORTABLE_GRAPH_PROJECTION_MODES, ), ) viewer_mode = cast( LiveViewerProjectionMode, _select_mode( live_viewer, field="live_viewer", default=default_viewer, allowed=LIVE_VIEWER_PROJECTION_MODES, ), ) if manual_mode != "disabled" and not manual_available: raise _unavailable("manual", manual_mode, "manual_render_config") if manual_mode == "auto" and not application_available: raise _unavailable("manual", manual_mode, "canonical_application") if graph_mode == "explicit" and not graph_available: raise _unavailable( "portable_graph", graph_mode, "portable_graph_render_config", ) if viewer_mode == "on-demand" and not viewer_available: raise _unavailable("live_viewer", viewer_mode, "live_viewer_runtime") return ProjectionPolicyV2( manual=manual_mode, portable_graph=graph_mode, live_viewer=viewer_mode, )