diff --git a/AGENTS.md b/AGENTS.md index f4ac69a..5e651d5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ deterministic mp4 render. Human edits survive AI regeneration of the base. - `pnpm reframe batch ` — one mp4 per row (row keys are overlay addresses like `nodes..`) - `pnpm reframe logo [--motion ] [--energy n] [--seed n]` — animate a logo into a sting (published CLI command; `packages/render-cli/src/logoSting.ts`) - `pnpm reframe labels ` — print the compiled event clock (every timeline label → exact seconds; the timing source for `audio.cues` and beat debugging) -- `pnpm reframe compile [-o out.json] [--stdin] [--code ""] [--json]` — bundle + validate eDSL source into SceneIR JSON, NO render (no ffmpeg/chromium; fast). On failure: a concise classified error (`bundle`/`eval`/`validation`), never the base64 bundle; `--json` makes it `{ok:false,error,kind}`. The in-process equivalent is exported as `reframe-video/compile` (`loadScene`/`loadSceneFromCode`/`checkDeterminism`, server-only). Entry `packages/render-cli/src/compile.ts`; loader `loadScene.ts`. +- `pnpm reframe compile [-o out.json] [--stdin] [--code ""] [--json]` — bundle + validate eDSL source into SceneIR JSON, NO render (no ffmpeg/chromium; fast). On failure: a concise classified error (`bundle`/`eval`/`validation`), never the base64 bundle; `--json` makes it `{ok:false,error,kind,issues?}` where `issues` is the structured validation problems (each `{code,path,message}` — e.g. `code:"unknown-blend", path:"nodes.box"`). The in-process equivalent is exported as `reframe-video/compile` (`loadScene`/`loadSceneFromCode`/`checkDeterminism`, server-only); a thrown `SceneValidationError` carries `.issues` (and `.problems` for back-compat), and `SceneLoadError.issues` propagates them across the scene bundle. Entry `packages/render-cli/src/compile.ts`; loader `loadScene.ts`. - `pnpm reframe frame [--t ] [-o out.png]` — render ONE frame at time `t` to a PNG (same renderer as `render`, no ffmpeg muxing; chromium only). For an agentic render-and-look loop (feed the frame back to a model). Reuses `renderFrameAt` (`frameLoop.ts`); entry `packages/render-cli/src/frame.ts`. - `pnpm reframe assemble [-o name] [--title "…"] [--bgm ] [--hold s] [--seed N]` — the **files → scene** path: probe each image/video for its real duration (ffprobe) and scaffold an editable montage scene `.ts` wiring `photoMontage` (clip-aware holds, no freeze) + an optional `title` + a music bed. Probed numbers are baked in → the emitted scene is a normal deterministic scene. Probe `packages/render-cli/src/media/probe.ts`; entry `assemble.ts`. - `pnpm reframe manifest [--json]` — dump the scene's **addressable surface**: every node (+ its `editableProps` and `animatedProps`), state, timeline label (+ `patchable` params), beat, and behavior, each with the overlay address that reaches it. The map an AI/human editor reads to patch a scene surgically (vs regenerating). Core `sceneManifest(compiled)` (`packages/core/src/manifest.ts`, exported); entry `packages/render-cli/src/manifest.ts`. diff --git a/CHANGELOG.md b/CHANGELOG.md index aa7129c..dd910c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,25 @@ versions may change them. ## [Unreleased] +## [0.6.43] - 2026-06-21 + +### Added + +#### Structured validation errors (`code` + `path` alongside the message) + +- `SceneValidationError` now carries **`.issues: ValidationIssue[]`** — each problem is + `{ code, path, message }` with a stable machine `code` (`unknown-blend`, `duplicate-node-id`, + `unknown-timeline-label`, `bad-duration`, …) and a `path` locator (`nodes.box`, + `timeline.beat(in)[0]`, `camera.zoom`, `audio.cues[0]`), so a consumer can categorize a + failure and point a UI at the offending element instead of parsing prose. +- Propagated in-process: `SceneLoadError.issues` carries them across the scene's own bundled + core (read as a plain property, since `instanceof` can't cross the bundle), and + `reframe compile --json` failures include `issues` (`{ ok:false, error, kind, issues? }`). + `ValidationIssue` is exported from `@reframe/core` and `reframe-video/compile`. +- **Fully back-compat / additive**: every message is byte-identical, and `.problems` (string[]) + + `.message` + the class identity are unchanged — the existing `toThrow(/…/)` suite passes + untouched. Validation isn't on the render path, so goldens are unaffected. + ## [0.6.42] - 2026-06-21 ### Added diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e39dae1..19fa8a0 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,6 @@ export * from "./ir.js"; export * from "./dsl.js"; -export { validateScene, validateComposition, SceneValidationError, PROPS_BY_TYPE } from "./validate.js"; +export { validateScene, validateComposition, SceneValidationError, PROPS_BY_TYPE, type ValidationIssue } from "./validate.js"; export { compileComposition, type CompiledComposition, diff --git a/packages/core/src/validate.ts b/packages/core/src/validate.ts index 81e1903..8377148 100644 --- a/packages/core/src/validate.ts +++ b/packages/core/src/validate.ts @@ -29,71 +29,92 @@ export const PROPS_BY_TYPE: Record = { group: COMMON_PROPS, }; +/** + * One validation problem, structured so a UI can point at the offending element + * (`path`) and a consumer can categorize (`code`) without parsing prose. `message` + * is the human string (unchanged). Codes are stable kebab strings; see the `add(...)` + * call sites in `validateScene`/`validateComposition`. + */ +export interface ValidationIssue { + code: string; + /** Locator for the offending element, e.g. `nodes.box`, `timeline.beat(in)[0]`, `camera.zoom`, `audio.cues[0]`. */ + path: string; + message: string; +} + export class SceneValidationError extends Error { - constructor(public problems: string[]) { - super(`Scene validation failed:\n${problems.map((p) => ` - ${p}`).join("\n")}`); + /** Structured form of the problems (code + path + message). */ + readonly issues: ValidationIssue[]; + /** Back-compat: the human messages, one per issue. */ + readonly problems: string[]; + constructor(issues: ValidationIssue[]) { + super(`Scene validation failed:\n${issues.map((i) => ` - ${i.message}`).join("\n")}`); this.name = "SceneValidationError"; + this.issues = issues; + this.problems = issues.map((i) => i.message); } } export function validateScene(ir: SceneIR): void { - const problems: string[] = []; + const issues: ValidationIssue[] = []; + const add = (code: string, path: string, message: string) => issues.push({ code, path, message }); const nodeById = new Map(); // video `start: "