from __future__ import annotations from typing import Any, Literal from pydantic import BaseModel, ConfigDict class StrictModel(BaseModel): model_config = ConfigDict(extra="forbid") class Metric(StrictModel): label: str value: str class RecordEnvelope(StrictModel): model_name: str schema_version: str | None = None payload: dict[str, Any] class FlowNode(StrictModel): id: str node_type: Literal[ "input", "contract", "operation", "worker", "attempt", "artifact", "validator", "validation", "decision", "checkpoint", "publication", "transaction", "readback", ] phase: Literal["phase-1", "phase-2", "phase-3", "delivery"] lane: str title: str subtitle: str status: str agent_role: str | None = None task_kind: str | None = None sequence: int x: int y: int width: int = 268 tags: list[str] metrics: list[Metric] record: RecordEnvelope class FlowEdge(StrictModel): id: str source: str target: str label: str | None = None kind: Literal["control", "artifact", "validation", "publication"] = "control" animated: bool = False class PhaseFrame(StrictModel): id: str title: str subtitle: str x: int y: int width: int height: int tone: Literal["coral", "blue", "purple", "green"] class RunSummary(StrictModel): script_build_id: int title: str status: str checkpoint: str root_trace_id: str publication_state: str node_count: int created_at: str updated_at: str class RunGraph(StrictModel): schema_version: Literal["script-build-visualization/v1"] data_mode: Literal["fake"] generated_at: str run: RunSummary frames: list[PhaseFrame] nodes: list[FlowNode] edges: list[FlowEdge] schema_catalog: dict[str, list[str]] enum_catalog: dict[str, list[str]] declared_gaps: list[str] class NodeDetail(StrictModel): data_mode: Literal["fake"] run_id: int node: FlowNode incoming: list[FlowEdge] outgoing: list[FlowEdge]