frameyard 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/GLOSSARY.md +40 -16
  3. package/README.md +54 -48
  4. package/dist/cli.js +23 -16
  5. package/dist/cli.js.map +1 -1
  6. package/dist/composition/descriptor.d.ts +17 -3
  7. package/dist/composition/descriptor.d.ts.map +1 -1
  8. package/dist/composition/descriptor.js +51 -28
  9. package/dist/composition/descriptor.js.map +1 -1
  10. package/dist/composition/format.d.ts +12 -0
  11. package/dist/composition/format.d.ts.map +1 -0
  12. package/dist/composition/format.js +28 -0
  13. package/dist/composition/format.js.map +1 -0
  14. package/dist/composition/index.d.ts +2 -1
  15. package/dist/composition/index.d.ts.map +1 -1
  16. package/dist/composition/index.js +1 -0
  17. package/dist/composition/index.js.map +1 -1
  18. package/dist/contracts.d.ts +55 -6
  19. package/dist/contracts.d.ts.map +1 -1
  20. package/dist/contracts.js +3 -0
  21. package/dist/contracts.js.map +1 -1
  22. package/dist/errors.d.ts +1 -1
  23. package/dist/errors.d.ts.map +1 -1
  24. package/dist/errors.js.map +1 -1
  25. package/dist/html/document-adapter.d.ts +3 -3
  26. package/dist/html/document-adapter.d.ts.map +1 -1
  27. package/dist/html/document-adapter.js +1 -1
  28. package/dist/html/document-adapter.js.map +1 -1
  29. package/dist/html/index.d.ts +1 -1
  30. package/dist/html/index.d.ts.map +1 -1
  31. package/dist/html/parse.d.ts +10 -2
  32. package/dist/html/parse.d.ts.map +1 -1
  33. package/dist/html/parse.js +14 -5
  34. package/dist/html/parse.js.map +1 -1
  35. package/dist/html/validate.d.ts +12 -1
  36. package/dist/html/validate.d.ts.map +1 -1
  37. package/dist/html/validate.js +233 -61
  38. package/dist/html/validate.js.map +1 -1
  39. package/dist/index.d.ts +4 -4
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +2 -2
  42. package/dist/index.js.map +1 -1
  43. package/dist/preview/main.d.ts +2 -2
  44. package/dist/preview/main.d.ts.map +1 -1
  45. package/dist/preview/main.js +35 -14
  46. package/dist/preview/main.js.map +1 -1
  47. package/dist/preview/timeline-data.d.ts +5 -1
  48. package/dist/preview/timeline-data.d.ts.map +1 -1
  49. package/dist/preview/timeline-host.d.ts.map +1 -1
  50. package/dist/preview/timeline-host.js +10 -0
  51. package/dist/preview/timeline-host.js.map +1 -1
  52. package/dist/preview/timeline-main.js +3 -2
  53. package/dist/preview/timeline-main.js.map +1 -1
  54. package/dist/project.d.ts +3 -5
  55. package/dist/project.d.ts.map +1 -1
  56. package/dist/project.js.map +1 -1
  57. package/dist/renderer/browser.d.ts +2 -2
  58. package/dist/renderer/browser.d.ts.map +1 -1
  59. package/dist/renderer/browser.js +10 -4
  60. package/dist/renderer/browser.js.map +1 -1
  61. package/dist/renderer/ffmpeg.d.ts +3 -0
  62. package/dist/renderer/ffmpeg.d.ts.map +1 -1
  63. package/dist/renderer/ffmpeg.js +122 -56
  64. package/dist/renderer/ffmpeg.js.map +1 -1
  65. package/dist/renderer/index.d.ts +40 -4
  66. package/dist/renderer/index.d.ts.map +1 -1
  67. package/dist/renderer/index.js +99 -24
  68. package/dist/renderer/index.js.map +1 -1
  69. package/dist/renderer/timeline.d.ts.map +1 -1
  70. package/dist/renderer/timeline.js +89 -0
  71. package/dist/renderer/timeline.js.map +1 -1
  72. package/dist/runtime/host.d.ts.map +1 -1
  73. package/dist/runtime/host.js +155 -31
  74. package/dist/runtime/host.js.map +1 -1
  75. package/dist/scaffold.d.ts.map +1 -1
  76. package/dist/scaffold.js +104 -101
  77. package/dist/scaffold.js.map +1 -1
  78. package/dist/server/composition-html.d.ts +13 -7
  79. package/dist/server/composition-html.d.ts.map +1 -1
  80. package/dist/server/composition-html.js +65 -16
  81. package/dist/server/composition-html.js.map +1 -1
  82. package/dist/server/server.d.ts.map +1 -1
  83. package/dist/server/server.js +26 -6
  84. package/dist/server/server.js.map +1 -1
  85. package/dist/server/timeline-preview-data.d.ts.map +1 -1
  86. package/dist/server/timeline-preview-data.js +11 -0
  87. package/dist/server/timeline-preview-data.js.map +1 -1
  88. package/dist/server/timeline-preview-html.d.ts.map +1 -1
  89. package/dist/server/timeline-preview-html.js +2 -1
  90. package/dist/server/timeline-preview-html.js.map +1 -1
  91. package/dist/timeline/resolve.d.ts +4 -3
  92. package/dist/timeline/resolve.d.ts.map +1 -1
  93. package/dist/timeline/resolve.js +14 -4
  94. package/dist/timeline/resolve.js.map +1 -1
  95. package/dist/timeline/schema.d.ts +7 -0
  96. package/dist/timeline/schema.d.ts.map +1 -1
  97. package/dist/timeline/schema.js +50 -0
  98. package/dist/timeline/schema.js.map +1 -1
  99. package/dist/version.d.ts +1 -1
  100. package/dist/version.js +1 -1
  101. package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
  102. package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
  103. package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
  104. package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
  105. package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
  106. package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
  107. package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
  108. package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
  109. package/docs/migration-0.4.md +3 -1
  110. package/docs/migration-0.8.md +163 -0
  111. package/docs/preview-testing.md +7 -0
  112. package/docs/v0.1-design.md +42 -34
  113. package/examples/blend/README.md +106 -0
  114. package/examples/blend/assets/ink.mp4 +0 -0
  115. package/examples/blend/compositions/aurora.html +55 -0
  116. package/examples/blend/compositions/backdrop.html +74 -0
  117. package/examples/blend/compositions/labels.html +149 -0
  118. package/examples/blend/scripts/make-ink.sh +26 -0
  119. package/examples/blend/timelines/blend.timeline.json +155 -0
  120. package/examples/blend/video.config.js +11 -0
  121. package/examples/demo/compositions/demo.html +243 -251
  122. package/examples/static/README.md +17 -0
  123. package/examples/static/compositions/still.html +37 -0
  124. package/examples/static/timelines/still.timeline.json +13 -0
  125. package/examples/static/video.config.js +9 -0
  126. package/examples/video/compositions/video-mp4.html +349 -357
  127. package/examples/video/compositions/video-overlay.html +337 -345
  128. package/examples/video/compositions/video.html +349 -357
  129. package/examples/webgl/compositions/webgl.html +205 -213
  130. package/package.json +2 -1
  131. package/skills/frameyard/SKILL.md +15 -13
  132. package/skills/frameyard/references/common-mistakes.md +29 -6
  133. package/skills/frameyard/references/controllers.md +5 -4
  134. package/skills/frameyard/references/deterministic-authoring.md +41 -36
  135. package/skills/frameyard/references/diagnostics.md +11 -3
  136. package/skills/frameyard/references/failure-recovery.md +25 -5
  137. package/skills/frameyard/references/json-results.md +46 -4
  138. package/skills/frameyard/references/timelines.md +16 -12
@@ -0,0 +1,32 @@
1
+ # Author compositions as HTML fragments
2
+
3
+ **Status:** Accepted. Amends [ADR-0007](./0007-make-html-documents-the-composition-unit.md) and [ADR-0008](./0008-share-the-composition-document-grammar.md).
4
+ **Date:** 2026-11-03
5
+
6
+ Frameyard 0.8 makes a composition a partial HTML fragment instead of a full HTML document. The author writes body content with one top-level `<fy-composition>` root, plus optional top-level props, controller, import maps, and styles. The server wraps that fragment in a fixed document shell at the composition URL, so preview and production keep the same relative-URL, stylesheet, and host semantics. This removes author-owned document boilerplate and makes the framework, not the author, responsible for the charset, title, import map, sizing style, format block, and host module.
7
+
8
+ ## Considered Options
9
+
10
+ - **Keep full HTML documents.** Rejected because every composition repeats a shell the framework already owns, and the head becomes a second place where import maps, metadata, and host markup can drift.
11
+ - **Accept both documents and fragments.** Rejected because two authoring shapes double validation, diagnostics, documentation, and agent examples for a pre-1.0 breaking release.
12
+ - **Inject a fragment into the preview container without a document.** Rejected for the reasons in ADR-0007: relative asset URLs, document styles, and head content would need rewriting. Serving a generated document at the real composition URL avoids that problem.
13
+ - **Generate full documents at build time.** Rejected because Frameyard has no build step and the preview server already renders composition entries per request.
14
+ - **Let projects configure the document head.** Rejected because the author goal is to stop managing the head; a configuration surface would recreate the problem in `video.config.js`.
15
+
16
+ ## Decision
17
+
18
+ - A composition entry is a partial HTML fragment. It contains exactly one top-level `<fy-composition>` element. Props blocks, the controller, and import maps must be top-level siblings; `<style>` and `<link rel="stylesheet">` may be top-level or inside the root.
19
+ - The fragment must not contain `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`, and must not use the reserved `data-fy-host` attribute or `data-fy="format"` block.
20
+ - The server generates a fixed document: `<!doctype html>`, `<html lang="en">`, a head with charset and title, the format block, the framework import map, author import maps in source order, the host style, hoisted author styles and stylesheet links in source order, and the host module; then the remaining fragment nodes in the body.
21
+ - Top-level author import maps are allowed and are hoisted after the framework map. Their JSON, attributes, and specifiers are validated; `frameyard` and `frameyard/` are reserved. Multiple import maps merge in document order, and the framework entries resolve first.
22
+ - Static validation parses the fragment in a body context and detects legacy document shells with a separate document parse. The shared composition reader takes a `document` or `fragment` root scope; the browser host keeps document scope, while Node validation uses fragment scope.
23
+ - The authoring change is a clean break in 0.8. A legacy full document fails with `COMPOSITION_DOCUMENT_SHELL` and a suggestion to the migration guide. There is no migration command.
24
+
25
+ ## Consequences
26
+
27
+ - `src/server/composition-html.ts` replaces host injection into an author document with document-shell generation and head hoisting.
28
+ - `src/html/validate.ts` gains fragment structure checks, shell detection, import-map validation, and reserved-attribute checks. `COMPOSITION_DOCUMENT_SHELL` and `IMPORT_MAP_INVALID` join the stable diagnostic codes.
29
+ - The composition reader's root scope is explicit. ADR-0008's shared grammar still applies; only the container differs between Node validation and the browser host.
30
+ - `.html` stays the composition extension and `project.json` keeps `kind: "html"`; the change is documented in `docs/migration-0.8.md` rather than encoded in the JSON contract.
31
+ - `examples/`, the `init` scaffold, the agent skill, and the test fixtures migrate to fragments. A baseline still comparison guards the wrapper against visual regressions.
32
+ - The browser host still reads a full document, so `readCompositionDescriptor` keeps its document-mode rules and the served document remains inspectable.
@@ -0,0 +1,99 @@
1
+ # Render timeless compositions as still images
2
+
3
+ **Status:** Accepted
4
+ **Date:** 2026-11-03
5
+
6
+ A composition that declares neither `duration-in-frames` nor `fps` is static. It
7
+ has no frame clock, no duration, and no audio or transcripts. Frameyard captures
8
+ it once in its initial state and publishes a PNG; inside a timeline the same
9
+ composition becomes an image layer that the clip's explicit duration controls.
10
+ A static composition is visual-only: `<img>`, CSS, SVG, and canvas are allowed;
11
+ `<video>`, `<audio>`, and `<fy-transcript>` are validation errors. Its controller
12
+ may `mount()` and validate props, but `update()` is not called and frame-state
13
+ variables are not set. CSS animations and transitions are paused at time 0 with
14
+ a warning. `data-fy-anim-*` is an error.
15
+
16
+ The metadata matrix is explicit. `duration-in-frames` plus `fps` is animated;
17
+ `duration-in-frames` without `fps` is animated with the project frame rate;
18
+ neither attribute is static; `fps` without `duration-in-frames` is
19
+ `COMPOSITION_INVALID`. `width` and `height` keep their current rules: declared
20
+ together or inherited from the project `format`. A static composition with no
21
+ dimensions in a project without `format` still fails with
22
+ `COMPOSITION_FORMAT_UNRESOLVED`.
23
+
24
+ `render <static-id>` writes `out/<id>.png` and accepts `--props`, `--overwrite`,
25
+ and `--json`. It rejects `--container`, `--codec`, `--encoder`, and
26
+ `--concurrency`. The render result is a discriminated union: `kind: "image"`
27
+ with `path`, `width`, `height`, `bytes`, `diagnostics`, and `elapsedMs`, or
28
+ `kind: "video"` with the existing video fields. `check` returns a matching
29
+ `kind: "static"` result with `framesChecked: 1` and no audio or transcript
30
+ fields. `still` treats a static composition as exactly frame 0; any other frame
31
+ is `FRAME_OUT_OF_RANGE`.
32
+
33
+ A timeline composition clip whose source is static must declare `duration` and
34
+ use `sourceIn: 0`, exactly like an image clip. The resolver gives it no intrinsic
35
+ duration and skips the frame-rate comparison. The renderer captures one
36
+ transparent PNG per composition/props pair and reuses it across segments and
37
+ workers through the existing image input path. The timeline still renders to
38
+ video; a fully static timeline is not converted to an image. Nested timelines
39
+ remain video layers in this version, although static compositions inside them
40
+ are still captured once. Timeline check inspects a static layer once and does
41
+ not seek it per frame.
42
+
43
+ ## Context
44
+
45
+ Composition rendering captured every frame of `duration-in-frames`, even when
46
+ the document never changed. Timeline rendering did the same for every active
47
+ composition layer in every segment. A long title card or a static background
48
+ therefore paid Chromium seeks, PNG captures, FFV1 encoding, and compositing for
49
+ identical frames. The existing `still` command already proved the single-capture
50
+ path, but it required the author to know the composition was static and to use a
51
+ different command.
52
+
53
+ The composition format had one required time fact, `duration-in-frames`, so
54
+ there was no way to express "this document has no time base". Treating missing
55
+ duration as an error protected authors from typos. This ADR makes the absence
56
+ meaningful and keeps the typo protection through the `fps`-without-duration
57
+ error, validation of time drivers, and a clear render output.
58
+
59
+ ## Considered options
60
+
61
+ - **An explicit `data-fy-static` marker with a frame sample.** Rejected: it keeps
62
+ `duration-in-frames` mandatory, so a still declares meaningless time metadata,
63
+ and it adds a second way to say what the missing attributes already say.
64
+ - **Runtime detection by comparing sampled frames.** Rejected: without a clock
65
+ there are no frames to sample, and with a clock the check is probabilistic
66
+ (an animation that only changes between samples escapes). The guarantee is
67
+ structural instead: a static composition has no time base and no time drivers.
68
+ - **Keep rendering static compositions as video.** Rejected: it preserves the
69
+ exact cost the change is meant to remove and forces a video container onto an
70
+ artifact that is one image.
71
+ - **Add `--video` to force the old path.** Rejected for now: a timeline already
72
+ produces video from an image clip, and the flag would make the output type
73
+ depend on a render flag rather than the composition contract.
74
+ - **Allow audio, transcripts, or embedded video in a static composition.**
75
+ Rejected: a PNG cannot carry them, and allowing them would make the same
76
+ source behave differently in a standalone render and in a timeline.
77
+ - **Export a fully static timeline as a PNG.** Rejected: a timeline is the
78
+ delivery unit and carries duration, audio, and sequencing even when its visual
79
+ content is constant.
80
+
81
+ ## Consequences
82
+
83
+ - `duration-in-frames` is no longer required on every composition. A composition
84
+ that declares neither time attribute is static; a composition that declares
85
+ only `fps` is invalid.
86
+ - Static compositions cannot use `data-fy-anim-*`, media, or transcripts. The
87
+ host reports `STATIC_ANIMATION_UNSUPPORTED`, `STATIC_MEDIA_UNSUPPORTED`, and
88
+ `STATIC_UPDATE_IGNORED`/`STATIC_ANIMATION_PAUSED` diagnostics.
89
+ - `render`, `check`, and `still` gain a static path. The video result keeps its
90
+ fields and adds `kind: "video"`; the image result and static check result are
91
+ new public shapes.
92
+ - Timeline composition clips are image-like when their source is static:
93
+ explicit duration, `sourceIn: 0`, no intrinsic source duration, and one
94
+ capture per props variant shared across segments.
95
+ - Preview loads a static composition once, disables its transport, and keeps a
96
+ static timeline layer loaded without seeking it.
97
+ - The design baseline, glossary, README, official skill, scaffold guide,
98
+ changelog, examples, and tests describe the new contract. Version 0.8.0 marks
99
+ the contract change.
@@ -1,5 +1,7 @@
1
1
  # Migrating to Frameyard 0.4
2
2
 
3
+ > Historical guide. Frameyard 0.8 changed compositions from full HTML documents to partial fragments; see [migration-0.8](./migration-0.8.md) for the current authoring shape.
4
+
3
5
  0.4 replaces JavaScript composition modules with declarative HTML documents. The CLI commands, props files, audio and transcript asset formats, rendering pipeline, and determinism guarantees are unchanged. This guide maps the old authoring surface to the new one.
4
6
 
5
7
  ## 1. Change the config entry
@@ -153,7 +155,7 @@ After:
153
155
  <fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
154
156
  ```
155
157
 
156
- Audio, video, and `<fy-transcript>` `src` values resolve from the project root. The controller `src` and ordinary HTML assets (`img`, stylesheets) resolve relative to the composition document.
158
+ Audio, video, and `<fy-transcript>` `src` values resolve from the project root. The controller `src` and ordinary HTML assets (`img`, stylesheets) resolve relative to the composition URL.
157
159
 
158
160
  ## 7. Rename media attributes
159
161
 
@@ -0,0 +1,163 @@
1
+ # Migrating to Frameyard 0.8
2
+
3
+ 0.8 changes compositions from full HTML documents to partial HTML fragments and adds static (timeless) compositions. The framework generates the document shell, import map, sizing style, format block, and host runtime. Timelines, props files, audio, transcripts, and media keep their existing model; the CLI adds PNG output for static compositions and discriminated result kinds.
4
+
5
+ ## Why
6
+
7
+ Every composition used to repeat `<!doctype html>`, `<html>`, `<head>`, `<meta charset>`, and `<body>`, and the author owned a `<head>` that the server already injected into. The fragment model keeps the same URLs, CSS, and relative asset behavior while making the framework responsible for the shell.
8
+
9
+ ## Before and after
10
+
11
+ Before:
12
+
13
+ ```html
14
+ <!doctype html>
15
+ <html>
16
+ <head>
17
+ <meta charset="utf-8">
18
+ <script type="application/json" data-fy="props">
19
+ { "title": "Hello" }
20
+ </script>
21
+ <style>
22
+ #title { animation: fade-up 1s ease-out both; }
23
+ </style>
24
+ <script type="importmap">
25
+ { "imports": { "demo-lib": "./demo-lib.js" } }
26
+ </script>
27
+ </head>
28
+ <body>
29
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
30
+ <h1 id="title" data-fy-text="title"></h1>
31
+ </fy-composition>
32
+ <script type="text/fy-controller" src="./hello.controller.js"></script>
33
+ </body>
34
+ </html>
35
+ ```
36
+
37
+ After:
38
+
39
+ ```html
40
+ <script type="application/json" data-fy="props">
41
+ { "title": "Hello" }
42
+ </script>
43
+ <style>
44
+ #title { animation: fade-up 1s ease-out both; }
45
+ </style>
46
+ <script type="importmap">
47
+ { "imports": { "demo-lib": "./demo-lib.js" } }
48
+ </script>
49
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
50
+ <h1 id="title" data-fy-text="title"></h1>
51
+ </fy-composition>
52
+ <script type="text/fy-controller" src="./hello.controller.js"></script>
53
+ ```
54
+
55
+ The fragment is the content that used to live in `<body>`, plus the head elements that remain author-owned: props, styles, stylesheet links, import maps, and the controller.
56
+
57
+ ## Rules
58
+
59
+ - Keep exactly one `<fy-composition>` element. It must be a top-level element, not nested in a `<div>`, `<template>`, or other element.
60
+ - Keep the props block, the controller, and import maps at the top level. They are metadata, not visual content.
61
+ - Keep `<style>` and `<link rel="stylesheet">` at the top level or inside the root. Top-level styles are hoisted into the generated head in source order; styles inside the root stay in the body.
62
+ - Do not add `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, or `<meta>`. A legacy document reports `COMPOSITION_DOCUMENT_SHELL`.
63
+ - Do not use `data-fy-host` or `data-fy="format"`; both are reserved for framework-injected markup.
64
+ - An import map must be a top-level `<script type="importmap">` with a JSON object. `frameyard` and `frameyard/` specifiers are reserved. Invalid maps report `IMPORT_MAP_INVALID`.
65
+ - `.html` is still the composition extension. `project.json` still reports `kind: "html"` and `entry` as the fragment path.
66
+ - Composition URLs, relative controller `src`, relative asset URLs, media paths, and transcript paths keep their existing resolution rules.
67
+
68
+ ## If the root was nested
69
+
70
+ Move the root to the top level and move the wrapper's styling into the root or into a top-level `<style>`:
71
+
72
+ Before:
73
+
74
+ ```html
75
+ <body>
76
+ <div class="page">
77
+ <fy-composition duration-in-frames="150">...</fy-composition>
78
+ </div>
79
+ </body>
80
+ ```
81
+
82
+ After:
83
+
84
+ ```html
85
+ <fy-composition duration-in-frames="150">
86
+ <div class="page">...</div>
87
+ </fy-composition>
88
+ ```
89
+
90
+ ## Static compositions
91
+
92
+ A composition that declares neither `duration-in-frames` nor `fps` is now
93
+ static. It is captured once and `render <id>` writes `out/<id>.png`.
94
+
95
+ If a composition was failing because `duration-in-frames` was missing, decide
96
+ which behavior you intended:
97
+
98
+ - For a still, remove `fps` too and use the composition directly or as a
99
+ timeline image clip.
100
+ - For video, add both `duration-in-frames` and `fps` (or let `fps` inherit from
101
+ the project `format`). Declaring `fps` without `duration-in-frames` remains an
102
+ error.
103
+
104
+ A static composition needs width and height, declared on the root or inherited
105
+ from the project `format`. It is visual-only: `<img>`, CSS, SVG, and canvas are
106
+ allowed; `<video>`, `<audio>`, and `<fy-transcript>` are validation errors. Its
107
+ controller may use `mount()` and `validateProps()`, but `update()` is not called
108
+ and frame-state variables are not set. Put audio, video, and captions on a
109
+ timeline instead.
110
+
111
+ `render` for a static composition rejects `--container`, `--codec`,
112
+ `--encoder`, and `--concurrency`. Use a timeline with an image/static
113
+ composition clip when the deliverable must be a video.
114
+
115
+ ## Static timeline clips
116
+
117
+ A clip whose composition source is static behaves like an image clip:
118
+
119
+ - `duration` is required;
120
+ - `sourceIn` must be `0`;
121
+ - `props`, transform, opacity, and blend mode still apply;
122
+ - the clip contributes no audio or transcripts.
123
+
124
+ The renderer captures one transparent PNG per composition/props pair and reuses
125
+ it across segments.
126
+
127
+ ## JSON result kinds
128
+
129
+ `render` now returns a discriminated union. Video results keep their fields and
130
+ add `kind: "video"`. Static composition results add `kind: "image"` with
131
+ `path`, `width`, `height`, `bytes`, `diagnostics`, and `elapsedMs`; they have no
132
+ `fps`, `durationInFrames`, `framesRendered`, `codec`, `container`, `encoder`, or
133
+ audio/transcript fields.
134
+
135
+ `check` also returns a union. Animated results add `kind: "animated"`. Static
136
+ results use `kind: "static"`, `metadata: { width, height, static: true }`,
137
+ `framesChecked: 1`, and no audio/transcript fields.
138
+
139
+ `still` on a static composition accepts only frame `0`; other frames fail with
140
+ `FRAME_OUT_OF_RANGE`.
141
+
142
+ Library consumers also see a type change: `FrameyardHost.metadata`,
143
+ `CompositionSourceInfo.metadata`, and browser sessions now expose
144
+ `ResolvedCompositionMetadata`, a union of animated metadata and
145
+ `{ width, height, static: true }`. Narrow with `metadata.static === true`
146
+ before reading `fps` or `durationInFrames`.
147
+
148
+ ## Error codes
149
+
150
+ - `COMPOSITION_DOCUMENT_SHELL`: remove the document shell. The diagnostic includes the element and line.
151
+ - `IMPORT_MAP_INVALID`: fix the import map placement, JSON, attributes, or reserved specifier.
152
+ - `PROPS_INVALID` / `CONTROLLER_INVALID`: move the block to the top level and keep exactly one controller.
153
+ - `COMPOSITION_INVALID`: the root is missing, duplicated, or nested, or `fps` is declared without `duration-in-frames`.
154
+ - `STATIC_ANIMATION_UNSUPPORTED`: remove `data-fy-anim-*` or declare `duration-in-frames` and `fps`.
155
+ - `STATIC_MEDIA_UNSUPPORTED`: move video, audio, or transcripts to a timeline, or declare time metadata.
156
+ - `STATIC_UPDATE_IGNORED`: move static initialization from `update()` to `mount()`.
157
+ - `STATIC_ANIMATION_PAUSED`: remove CSS animations/transitions from a static composition; capture uses time 0.
158
+
159
+ `check`, `still`, and `render` run the same validation before Chromium. Run `npx frameyard check <id> --json` after editing each composition.
160
+
161
+ ## No migration command
162
+
163
+ 0.8 does not ship a migration command. The transformation is mechanical but depends on what the old `<head>` contained and whether the root was nested. Use this guide, migrate one composition at a time, and let `check` report the remaining structural errors.
@@ -12,6 +12,10 @@ expose the same seek/settle contract the renderer uses.
12
12
  `settle()`, and `dispose()`.
13
13
  - `seek()` is synchronous. Call `await settle()` before reading pixels; `load()`
14
14
  resets the host, so apply props through it.
15
+ - A static composition reports `metadata: { width, height, static: true }`.
16
+ `seek()` is a no-op, `settle()` resolves, and the page disables the transport.
17
+ Wait for the static composition's own marker instead of `waitForFrame()`,
18
+ which expects a controller-updated `#box`.
15
19
 
16
20
  ## Timeline preview
17
21
 
@@ -21,6 +25,9 @@ expose the same seek/settle contract the renderer uses.
21
25
  - Order: `await ready`, `await load()`, `await seek(frame)`, `await settle()`.
22
26
  - The non-embedded page owns transport and audio; only the embedded host exposes
23
27
  `window.__fyTimeline`.
28
+ - A static composition source carries `static: true` and no
29
+ `durationInFrames`. The host loads the layer once and skips per-frame seek;
30
+ the layer remains visible while the timeline playhead moves.
24
31
 
25
32
  ## Waiting for a frame
26
33
 
@@ -2,9 +2,9 @@
2
2
 
3
3
  **Status:** Accepted
4
4
  **Initial acceptance:** 2026-10-27
5
- **Last design revision:** 2026-10-31
5
+ **Last design revision:** 2026-11-03
6
6
 
7
- This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.6. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
7
+ This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.8 and extended with static clip blend modes. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
8
8
 
9
9
  ## 1. Product contract
10
10
 
@@ -16,7 +16,7 @@ The target use cases are product demos, social clips, animated diagrams, data vi
16
16
 
17
17
  The core success criterion is an end-to-end example in which a developer can:
18
18
 
19
- 1. Create reusable, 1920×1080 HTML compositions with fixed metadata.
19
+ 1. Create reusable HTML compositions with a fixed duration and a format they either declare or inherit from the project.
20
20
  2. Animate them with HTML, CSS, SVG, and, where needed, a seek-safe controller for Canvas 2D and WebGL.
21
21
  3. Supply JSON props and synchronized audio.
22
22
  4. Assemble compositions and local media into a timeline with tracks, clips, trims, layers, and nested timelines.
@@ -30,18 +30,20 @@ The agent-enabled success criterion additionally requires an agent using the off
30
30
 
31
31
  ### Project
32
32
 
33
- - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition document or in external modules.
33
+ - Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition fragment or in external modules.
34
34
  - `video.config.js` explicitly maps project IDs to source files. There is no directory scanning. It has three optional maps:
35
35
  - `format`: the project's fixed `width`, `height`, and `fps`.
36
- - `compositions`: IDs mapped to `.html` composition documents.
36
+ - `compositions`: IDs mapped to `.html` composition fragments.
37
37
  - `timelines`: IDs mapped to JSON timeline documents.
38
- - `format` is required when a project has timelines. Every timeline uses it. A composition used in a timeline must share the project frame rate exactly; a different size is fitted and transformed by the clip. A standalone composition render keeps using the composition's own metadata.
38
+ - `format` is required when a project has timelines. Every timeline uses it. It also fills `width`, `height`, and `fps` for animated compositions that omit them; a declared value always wins. A composition that declares an `fps` must share the project frame rate exactly; a different size is fitted and transformed by the clip. A composition that declares neither `duration-in-frames` nor `fps` is static; it uses width and height, declared or inherited, and ignores the inherited fps. A composition that declares no dimensions in a project without `format` cannot render and fails with `COMPOSITION_FORMAT_UNRESOLVED`.
39
39
  - Composition and timeline IDs share one namespace so `render <id>`, `check <id>`, and `still <id>` are unambiguous. The config rejects duplicates.
40
40
  - A project may contain any number of compositions and timelines.
41
41
 
42
42
  ### Composition
43
43
 
44
- - A composition is a full HTML document with one `<fy-composition>` root. The root declares fixed `width`, `height`, `fps`, and `duration-in-frames` metadata. Props cannot change this metadata.
44
+ - A composition is a partial HTML fragment with one top-level `<fy-composition>` root. The framework wraps it in the document shell, import map, sizing style, and host runtime at serve and render time. `width`, `height`, and `fps` are optional and inherit from the project `format` when absent; declared values win. `width` and `height` are declared together or not at all. Props cannot change metadata.
45
+ - A composition that declares neither `duration-in-frames` nor `fps` is static: it has no frame clock, is captured once in its initial state, and renders to a PNG. Its controller may `mount()` and validate props, but `update()` is not called and frame-state variables are not set. CSS animations and transitions are paused at time 0 with a warning; `data-fy-anim-*`, `<video>`, `<audio>`, and `<fy-transcript>` are errors. `fps` without `duration-in-frames` is an error. See [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
46
+ - Props blocks, the controller, and import maps are top-level siblings of the root. Top-level `<style>` and `<link rel="stylesheet">` elements are hoisted into the generated head; styles inside the root stay in the body. A fragment must not contain `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, `<meta>`, or the reserved `data-fy-host` attribute.
45
47
  - Default props are a JSON block, `<script type="application/json" data-fy="props">`. A load's JSON props merge shallowly over the defaults, are validated as JSON, deep-frozen, and fixed for that load.
46
48
  - Props bind into the document through explicit `data-fy-*` attributes: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat`. Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties. Bindings apply once per load; props are immutable.
47
49
  - CSS and the Web Animations API are the animation language. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in composition frames.
@@ -59,13 +61,14 @@ The agent-enabled success criterion additionally requires an agent using the off
59
61
  - a composition, identified by its project ID;
60
62
  - a nested timeline, identified by its project ID;
61
63
  - a local video, image, or audio asset.
62
- - A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings.
63
- - A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity and transform apply to the whole nested result. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline in 0.6.
64
- - Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images are static and require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
65
- - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box. No keyframes, blend modes, masks, filters, or other effects in 0.6.
66
- - Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
64
+ - A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings. A clip whose composition source is static behaves like an image: it must declare `duration`, must use `sourceIn: 0`, has no intrinsic source duration, and contributes no audio or transcripts. The renderer captures one transparent PNG per composition/props pair and reuses it across segments.
65
+ - A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity, transform, and blend mode apply to the whole nested result. A blend mode inside a nested timeline sees only that timeline's own backdrop; a transparent nested timeline does not see the parent's layers, and a declared nested background is part of the group. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline.
66
+ - Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images and static composition clips require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
67
+ - Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box.
68
+ - Visual clips may declare a static `blend` mode. It uses CSS `mix-blend-mode` semantics against the accumulated backdrop of all lower layers and the timeline background, respects the clip's source alpha, and defaults to `normal`. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. There are no keyframes, masks, filters, or other effects.
69
+ - Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Static compositions contribute no audio; clip audio settings on a static composition are ignored with a warning. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
67
70
  - A timeline's duration is derived from the latest clip end. An explicit `durationInFrames` may extend it but not cut clips. A timeline with no clips is invalid; an audio-only timeline is valid and renders black video with audio.
68
- - A timeline background is transparent by default. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
71
+ - A timeline background is transparent by default. Blend modes see the accumulated backdrop including this background. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
69
72
  - The preview provides a read-only timeline view: tracks, clips, playhead, program monitor, and diagnostics. It is not a visual editor.
70
73
 
71
74
  ## 3. Deterministic execution
@@ -78,8 +81,9 @@ The timeline frame is the master clock. A composition's frame is derived from th
78
81
  - JavaScript animation runs in the controller's `update()`, which must be synchronous and seek-safe. It cannot depend on previously visited frames.
79
82
  - Declarative bindings apply once per load, before the controller mounts. Controller-created DOM is the controller's responsibility.
80
83
  - Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
81
- - Timeline clips use static transforms. Timeline-level animation, transitions, effects, and keyframes are not part of 0.6.
84
+ - Timeline clips use static transforms and static blend modes. Timeline-level animation, transitions, and other effects are not supported.
82
85
  - Preview and production seek composition clips explicitly. The timeline compositor is deterministic given the captured clip frames, media frames, clip settings, and segment boundaries.
86
+ - The resolved format—declared width, height, and fps combined with the project format—is part of the immutable frame environment, alongside props and loaded assets. Layouts that use viewport units or percentages therefore depend on the resolved format by design; preview and production always resolve the same values.
83
87
  - Assets, remote data, images, fonts, generated audio, transcripts, and media are resolved before frame production. For a segmented timeline render, each segment's composition and nested-timeline clips are loaded and settled before that segment's frames are captured; no frame production depends on a new network request. Network is blocked during capture.
84
88
  - AI model inference is never part of the loading phase, controller lifecycle, or frame production.
85
89
  - The baseline supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video. Canvas 2D and WebGL are drawn from the current frame by the controller.
@@ -96,23 +100,23 @@ Preview and production share composition behavior and capture composition frames
96
100
 
97
101
  ## 4. Preview and production rendering
98
102
 
99
- Preview and production execute the same composition documents and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
103
+ Preview and production execute the same composition sources and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
100
104
 
101
105
  The preview provides:
102
106
 
103
- - Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics.
104
- - Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics.
107
+ - Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics. A static composition loads once and disables the transport.
108
+ - Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics. A static composition layer stays loaded and is not seeked per frame.
105
109
  - A bounded pool of active composition and nested-timeline clips. It loads clips around the playhead instead of preloading a whole long timeline.
106
110
 
107
- The preview server serves each composition HTML document at its project path and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
111
+ The preview server serves each composition fragment at its project path, wraps it in the fixed document shell, and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
108
112
 
109
- Production composition rendering is unchanged: one Chromium session per composition, explicit frame seeks, PNG capture, and FFmpeg encoding.
113
+ Production composition rendering uses one Chromium session per composition. An animated composition seeks every frame, captures PNGs, and encodes them with FFmpeg. A static composition loads once, captures one PNG, and publishes it without FFmpeg or an encoder.
110
114
 
111
115
  Production timeline rendering is segmented and parallel:
112
116
 
113
117
  1. Run static timeline validation and resolve the render plan.
114
118
  2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
115
- 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background.
119
+ 3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers.
116
120
  4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
117
121
  5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
118
122
  6. Mix the timeline audio once with FFmpeg and mux it into the final output.
@@ -122,7 +126,7 @@ The default timeline worker count is 8, configurable up to the machine's capacit
122
126
 
123
127
  The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
124
128
 
125
- `check` and `still` use the same clip capture and timeline composite path for requested frames. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. `still` captures one or more requested frames as PNGs without encoding a video.
129
+ `check` and `still` use the same clip capture and timeline composite path for requested frames. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. A static composition has one state and is inspected once. `still` captures one or more requested frames as PNGs without encoding a video; a static composition exposes only frame 0.
126
130
 
127
131
  ## 5. Audio and output
128
132
 
@@ -135,13 +139,13 @@ A timeline has one audio mix. It combines:
135
139
 
136
140
  Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final output. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
137
141
 
138
- Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
142
+ Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition URL.
139
143
 
140
144
  Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change timeline duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or the timeline deliberately.
141
145
 
142
- 0.6 produces MP4, MOV, MKV, or WebM files. The default is MP4 with H.264 video and AAC audio; HEVC and VP9 are opt-in, and WebM is VP9 with Opus. HEVC in MP4 and MOV keeps the `hvc1` sample entry and is not playable in browsers without platform HEVC decoding. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments.
146
+ Timelines and animated compositions produce MP4, MOV, MKV, or WebM files. The default is MP4 with H.264 video and AAC audio; HEVC and VP9 are opt-in, and WebM is VP9 with Opus. HEVC in MP4 and MOV keeps the `hvc1` sample entry and is not playable in browsers without platform HEVC decoding. Frameyard exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. A static composition produces one PNG and uses no encoder.
143
147
 
144
- PNG output through `still`, including a bounded set of inspection frames, is a debugging artifact, not general image-sequence support.
148
+ PNG output through `still`, including a bounded set of inspection frames, is a debugging artifact, not general image-sequence support. A standalone static composition renders to one PNG; this is the composition's production artifact, not an image sequence.
145
149
 
146
150
  A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, invalid composition output, segment encoding failure, or scratch failure. Diagnostics identify the timeline, clip, segment, and, where applicable, the frame. The CLI exits nonzero, cleans temporary files, and never reports a partial video as successful. Existing output files are not overwritten unless explicitly requested.
147
151
 
@@ -151,16 +155,16 @@ Project and composition commands are:
151
155
 
152
156
  - `init`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts.
153
157
  - `dev`: start the preview with composition and timeline modes.
154
- - `render <id> [--out video]`: render a timeline or a composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`.
155
- - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding.
156
- - `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
158
+ - `render <id> [--out video]`: render a timeline, an animated composition, or a static composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`. A static composition writes `out/<id>.png` and rejects `--container`, `--codec`, `--encoder`, and `--concurrency`. The JSON result is a discriminated union with `kind: "video"` or `kind: "image"`.
159
+ - `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding. A static composition is loaded and inspected once and returns `kind: "static"` with `framesChecked: 1`.
160
+ - `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. A static composition exposes exactly frame 0. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
157
161
  - `doctor`: inspect core requirements and optional integrations without repairing or downloading anything.
158
162
 
159
- `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe document reader. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
163
+ `check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe reader with document and fragment scopes. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
160
164
 
161
165
  Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr.
162
166
 
163
- The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It injects an import map and composition host into composition entries and an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
167
+ The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It wraps each configured composition fragment in the fixed document shell, injects the framework import map and composition host, and serves an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
164
168
 
165
169
  Frameyard uses one repository and one publishable npm package with four internal layers:
166
170
 
@@ -257,12 +261,12 @@ Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` re
257
261
  Tests prioritize public behavior and end-to-end evidence over implementation-only unit coverage:
258
262
 
259
263
  - HTML compositions validate metadata, default props, bindings, conditionals, repeats, and controller declarations, including inline/external parity and the exactly-one-controller rule.
260
- - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium.
264
+ - Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium. It resolves declared format values against the project format and reports an unresolved format to the command rather than the document. A composition with neither time attribute is static; `fps` without `duration-in-frames`, `data-fy-anim-*`, media, and transcripts on a static composition are errors.
261
265
  - Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, bounds, media, transform, audio, and props errors before Chromium.
262
266
  - Diagnostics carry stable codes and source locations or JSON paths; JSON results expose the composition or timeline entry, clips, tracks, and top-level diagnostics.
263
267
  - Sequential, individual, random-order, and parallel composition renders produce equivalent images; timeline segment workers produce ordered output and identical captured composition frames.
264
268
  - Determinism checks compare captured composition pixels or stills. The final encoded bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
265
- - Preview and renderer use the same composition document and host behavior; timeline preview uses the same active clip hosts.
269
+ - Preview and renderer use the same composition source and host behavior; timeline preview uses the same active clip hosts.
266
270
  - CSS, controller JavaScript, SVG, Canvas, WebGL, embedded video, and audio frames seek correctly.
267
271
  - Frame-scheduled `data-fy-anim-*` animations and frame-state CSS variables produce the expected frames.
268
272
  - Timeline capture preserves alpha for transparent compositions; FFV1 intermediates decode pixel-identically to the captured frames.
@@ -270,7 +274,7 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
270
274
  - Nested timelines preserve group opacity and transform semantics; cycles fail static validation.
271
275
  - Media clips trim and fit deterministically; images require durations; source overruns fail.
272
276
  - Props overrides for timeline composition clips merge over clip props and are reflected in the render.
273
- - `check` exercises every frame; `still` follows the clip capture and timeline composite path.
277
+ - `check` exercises every animated frame and inspects a static composition once; `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
274
278
  - The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact output and clean scratch, documenting composition-heavy performance above 30 minutes.
275
279
  - Golden-frame tests cover representative composition and timeline visual behavior.
276
280
  - Normal CI uses fake `qwen-tts` and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
@@ -281,7 +285,7 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
281
285
 
282
286
  These are scope boundaries, not permanent rejections:
283
287
 
284
- - Transitions, effects, keyframes, blend modes, masks, and audio fades
288
+ - Transitions, filters, keyframes, masks, and audio fades
285
289
  - Full visual timeline editing, drag-and-drop, clip handles, and undo/redo
286
290
  - Timeline-level variables and prop overrides for compositions inside nested timelines
287
291
  - Track solo and video-track visibility
@@ -305,6 +309,10 @@ These are scope boundaries, not permanent rejections:
305
309
  - Dynamic per-frame asset loading or AI inference
306
310
  - Unbounded batch still capture; `still` accepts at most 32 inspection frames per invocation and is not an image-sequence exporter
307
311
  - Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
312
+ - Audio, transcripts, or embedded video in a static composition; use a timeline
313
+ - A `--video` escape hatch for static composition renders; use a timeline image clip
314
+ - Exporting a fully static timeline as a PNG
315
+ - Recursive static optimization of nested timelines
308
316
  - Built-in TypeScript transpilation and npm dependency optimization
309
317
  - Rendering untrusted compositions
310
318
  - Bit-identical cross-platform encoded output or AI regeneration
@@ -312,4 +320,4 @@ These are scope boundaries, not permanent rejections:
312
320
 
313
321
  ## 13. Release posture
314
322
 
315
- Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
323
+ Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md). Static clip blend modes are recorded in [ADR 0014](./adr/0014-compose-timeline-clips-with-css-blend-modes.md). Optional composition `width`, `height`, and `fps` with project-format inheritance are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md). 0.8.0 changes compositions to HTML fragments, inherits their format from the project, and renders timeless compositions as still images; the contracts are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md), [ADR 0016](./adr/0016-author-compositions-as-html-fragments.md), and [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).