Implements the 2026-04-02 transformation spec (Phases 1-8) and fixes all critical bugs found during 5-pipeline E2E testing. Governance & Decision Intelligence: - Pipeline-specific stage order in checkpoint (replaces global STAGES list) - Provider scoring engine (lib/scoring.py) with 7-dimension weighted ranking - Decision log artifact enforced at proposal/idea stage across all 10 pipelines - Delivery promise classifier prevents silent motion-to-still downgrades - Structured shot language in scene_plan schema (camera, lens, lighting, DOF) - Variation checker and slideshow risk scorer block samey output before render - Creative intake, capability extension, and creative-intake meta skills - Final self-review artifact with 5 mandatory checks before presenting output - Source media review contract for user-supplied footage Render & Theme System: - Remotion AnimatedBackground now derives colors from playbook (no more hardcoded dark blue fintech gradient on every video) - video_compose builds custom ThemeConfig from playbook YAML colors/fonts — custom playbooks flow through to Remotion automatically - Explainer component wires theme to all child components (charts, cards, etc.) - resolveAsset() handles absolute paths on Windows/Unix via file:// URIs - RENDERER_FAMILY_MAP synced with actual Remotion compositions Critical Bug Fixes: - Windows npx subprocess: run_command() resolves .cmd wrappers via shutil.which() - Silent renderer downgrade: Remotion failure now returns explicit error with options instead of silently falling back to FFmpeg - .env inline comment parsing strips trailing # comments from API keys - concat_path UnboundLocalError in video_compose finally block - audio_mixer and showcase_card capture=True kwarg bug - Selector estimate_cost() calls fixed (_select_tool -> _select_best_tool) - asset_manifest schema expanded with provider, license, subtype fields - screen-demo subtitle_gen moved from required to optional tools - Duration drift detection in post-render final review (>25% warns)
138 lines
4.3 KiB
Python
138 lines
4.3 KiB
Python
"""Pipeline manifest loader.
|
|
|
|
Loads and validates pipeline YAML manifests from pipeline_defs/.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
|
|
import yaml
|
|
import jsonschema
|
|
|
|
PIPELINE_DEFS_DIR = Path(__file__).resolve().parent.parent / "pipeline_defs"
|
|
SCHEMA_PATH = (
|
|
Path(__file__).resolve().parent.parent
|
|
/ "schemas"
|
|
/ "pipelines"
|
|
/ "pipeline_manifest.schema.json"
|
|
)
|
|
|
|
|
|
def _load_manifest_schema() -> dict:
|
|
with open(SCHEMA_PATH) as f:
|
|
return json.load(f)
|
|
|
|
|
|
def load_pipeline(name: str, defs_dir: Optional[Path] = None) -> dict[str, Any]:
|
|
"""Load and validate a pipeline manifest by name.
|
|
|
|
Args:
|
|
name: Pipeline name (without .yaml extension).
|
|
defs_dir: Override directory for pipeline definitions.
|
|
|
|
Returns:
|
|
Validated pipeline manifest dict.
|
|
"""
|
|
defs_dir = defs_dir or PIPELINE_DEFS_DIR
|
|
path = defs_dir / f"{name}.yaml"
|
|
if not path.exists():
|
|
raise FileNotFoundError(f"Pipeline manifest not found: {path}")
|
|
|
|
with open(path) as f:
|
|
manifest = yaml.safe_load(f)
|
|
|
|
schema = _load_manifest_schema()
|
|
jsonschema.validate(instance=manifest, schema=schema)
|
|
|
|
return manifest
|
|
|
|
|
|
def list_pipelines(defs_dir: Optional[Path] = None) -> list[str]:
|
|
"""List all available pipeline manifest names."""
|
|
defs_dir = defs_dir or PIPELINE_DEFS_DIR
|
|
return [p.stem for p in defs_dir.glob("*.yaml")]
|
|
|
|
|
|
def get_stage_order(manifest: dict) -> list[str]:
|
|
"""Extract the ordered list of stage names from a manifest."""
|
|
return [stage["name"] for stage in manifest["stages"]]
|
|
|
|
|
|
def get_required_tools(manifest: dict) -> set[str]:
|
|
"""Collect all preferred + fallback + available tools across all stages."""
|
|
tools: set[str] = set()
|
|
for stage in manifest["stages"]:
|
|
tools.update(stage.get("preferred_tools", []))
|
|
tools.update(stage.get("fallback_tools", []))
|
|
tools.update(stage.get("tools_available", []))
|
|
return tools
|
|
|
|
|
|
def get_stage_skill(manifest: dict, stage_name: str) -> Optional[str]:
|
|
"""Get the skill path for an instruction-driven stage."""
|
|
for stage in manifest["stages"]:
|
|
if stage["name"] == stage_name:
|
|
return stage.get("skill")
|
|
return None
|
|
|
|
|
|
def get_stage_review_focus(manifest: dict, stage_name: str) -> list[str]:
|
|
"""Get the review focus items for a stage."""
|
|
for stage in manifest["stages"]:
|
|
if stage["name"] == stage_name:
|
|
return stage.get("review_focus", [])
|
|
return []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Capability-Extension Enforcement
|
|
# ---------------------------------------------------------------------------
|
|
|
|
class ExtensionNotPermitted(PermissionError):
|
|
"""Raised when a capability extension is used but not permitted by the pipeline."""
|
|
|
|
|
|
def check_extension_permitted(
|
|
manifest: dict,
|
|
extension_type: str,
|
|
) -> None:
|
|
"""Enforce that a capability extension is permitted by the pipeline manifest.
|
|
|
|
Args:
|
|
manifest: Loaded pipeline manifest dict.
|
|
extension_type: One of 'custom_scripts', 'custom_playbooks',
|
|
'custom_skills', 'custom_tools'.
|
|
|
|
Raises:
|
|
ExtensionNotPermitted: If the extension is not allowed.
|
|
"""
|
|
valid_extensions = {"custom_scripts", "custom_playbooks", "custom_skills", "custom_tools"}
|
|
if extension_type not in valid_extensions:
|
|
raise ValueError(
|
|
f"Unknown extension type {extension_type!r}. "
|
|
f"Valid types: {sorted(valid_extensions)}"
|
|
)
|
|
|
|
extensions = manifest.get("extensions", {})
|
|
if not extensions.get(extension_type, False):
|
|
raise ExtensionNotPermitted(
|
|
f"Pipeline {manifest.get('name', 'unknown')!r} does not permit "
|
|
f"{extension_type}. Set extensions.{extension_type}: true in the "
|
|
f"pipeline manifest to allow this."
|
|
)
|
|
|
|
|
|
def get_permitted_extensions(manifest: dict) -> dict[str, bool]:
|
|
"""Return the extension permission flags for a pipeline."""
|
|
defaults = {
|
|
"custom_scripts": False,
|
|
"custom_playbooks": False,
|
|
"custom_skills": False,
|
|
"custom_tools": False,
|
|
}
|
|
extensions = manifest.get("extensions", {})
|
|
return {k: extensions.get(k, v) for k, v in defaults.items()}
|