from __future__ import annotations from dataclasses import dataclass from pathlib import Path from typing import Any import yaml class WorkflowSpecError(ValueError): """Raised when a workflow spec cannot be parsed or is incomplete.""" @dataclass(slots=True) class WorkflowSpec: name: str provider: str frontmatter: dict[str, Any] body: str source_path: Path def _split_frontmatter(raw_text: str) -> tuple[str, str]: if not raw_text.startswith("---"): raise WorkflowSpecError("workflow spec must start with frontmatter") parts = raw_text.split("\n---", 1) if len(parts) != 2: raise WorkflowSpecError("workflow spec frontmatter is not terminated") frontmatter_text = parts[0][4:] body = parts[1].lstrip("\r\n") return frontmatter_text, body def load_workflow_spec(path: str | Path) -> WorkflowSpec: source_path = Path(path) raw_text = source_path.read_text(encoding="utf-8") frontmatter_text, body = _split_frontmatter(raw_text) payload = yaml.safe_load(frontmatter_text) or {} if not isinstance(payload, dict): raise WorkflowSpecError("workflow spec frontmatter must be a mapping") if True in payload and "on" not in payload: payload["on"] = payload.pop(True) name = str(payload.get("name") or "").strip() provider = str(payload.get("provider") or "").strip() if not name: raise WorkflowSpecError("workflow spec is missing required field: name") if not provider: raise WorkflowSpecError("workflow spec is missing required field: provider") return WorkflowSpec( name=name, provider=provider, frontmatter=payload, body=body, source_path=source_path, )