Skip to content

Commit 078c951

Browse files
committed
docs(skills): correct composition-contract claims the code contradicts
The runtime has absorbed a series of authoring mistakes over time (array `__timelines`, a missing root `data-start`, a mistyped timeline key, unnested child timelines), and `init.ts` says so in its own comments. The skills kept teaching the old rules, so agents were told to satisfy constraints that no longer exist and, in four cases, were told to do things the gates now reject. Four instructions actively cost an agent a failing run: - `variables-and-media.md` said to add `crossorigin="anonymous"` for canvas capture. `media_crossorigin_breaks_preview` rejects it unconditionally, with no suppression, even for the readback case its own fixHint mentions. - `determinism-rules.md` banned building a timeline inside an async callback. `gsap_timeline_registered_before_async_build` calls that build the documented contract; what it rejects is registering the key before the build finishes. - `determinism-rules.md` banned `gsap.set()` on later-scene clips as "not in the DOM at page load". They are in the DOM, and two lint fixHints instruct exactly that set, so following the rule left two findings unfixable. - `creator-editing-recipes.md` shipped 12 copyable `<audio>`/`<video>` snippets with no `id`. `media_missing_id` is an error, and an id-less `<audio>` is never picked up by the mixer, so a copied recipe renders silent. Corrected elsewhere, in every place each claim appeared: - `data-track-index` is a Studio display lane, not a timing constraint. The render never reads it, no rule forbids same-track overlap, and the allocation ritual around it is removed. 11 sites. - `class="clip"` is a layout and tooling convention. Visibility keys on `data-start`. The class stays recommended, because the shared `.clip` rule is what gives a scene its box. - Timed elements may nest. The "must be DIRECT children of the root" rule was false; what is true is that only root children with `data-start` get automatic layout, which is why the `full-screen-motion.md` background snippet rendered 0px tall. That snippet now carries the CSS it always needed. - The visibility window is half-open, `[start, start + duration)`, pinned by `init.test.ts`. It was documented as inclusive, so an animation landing exactly on `data-duration` never rendered its last frame. - The relative-start grammar has four silent failure modes, all resolving to 0, none checked by lint. `intro-0.5` without spaces parses as an id, references resolve document-wide, cycles resolve to 0, and an unresolvable target duration lands on the target's start rather than its end. - Sub-composition host `data-width`/`data-height` are backfilled by the compiler, and a host id that differs from the file's is supported. Both were "Required". - The root background is stripped only on the layered-composite path (HDR content or shader transitions), not on every render. - Sub-composition Pitfall 3 kept a symptom fixed by #1886 and pinned by `packages/producer/tests/sub-comp-class-selector/`. The lint rule is still the reason to use `#root`; the render is not. - Cross-file duplicate media ids no longer render blank for `src`-bearing media, which the compiler stamps with `data-hf-render-id`. Media using `<source>` children is not stamped, so that case is called out instead. The three workflow `assemble-index.mjs` and `transitions.mjs` copies attributed their same-track lane rule to `timeline_track_too_dense`, which counts elements per lane and never compares windows. The checks are kept, since each workflow's frame injector assumes one clip per lane, but they now say it is their own convention rather than a framework rule. The scaffolded project instructions written into every `hyperframes init` project carried the same two false lines and are corrected in lockstep. `coreSkillContent.test.ts` pinned the literal sentence that listed root `data-start` as required, so every correction here would have reddened CI to protect a false claim. It now pins structure plus the regression it genuinely catches.
1 parent 32d58a7 commit 078c951

22 files changed

Lines changed: 173 additions & 97 deletions

File tree

packages/cli/src/commands/coreSkillContent.test.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,17 @@ const REPO_ROOT = join(fileURLToPath(new URL(".", import.meta.url)), "..", "..",
88
const read = (...parts: string[]): string => readFileSync(join(REPO_ROOT, ...parts), "utf8");
99

1010
describe("hyperframes-core contract docs", () => {
11-
it("keeps root data-start in the minimal composition skeleton", () => {
11+
it("keeps a runnable root in the minimal composition skeleton", () => {
1212
const minimal = read("skills", "hyperframes-core", "references", "minimal-composition.md");
1313

14-
expect(minimal).toMatch(/data-composition-id="main"[\s\S]{0,300}data-start="0"/);
15-
expect(minimal).toContain('Root `<div>` with `data-composition-id`, `data-start="0"`');
14+
// Structural pin: the skeleton must still declare a root the runtime can find
15+
// and size. The prose around it is deliberately not pinned: asserting exact
16+
// sentences here made every docs correction a CI failure, and the sentence this
17+
// replaces ("Root <div> with data-composition-id, data-start=\"0\"") listed
18+
// data-start as required when the runtime stamps it (runtime/init.ts).
19+
expect(minimal).toMatch(/data-composition-id="main"/);
20+
expect(minimal).toMatch(/data-width="1920"[\s\S]{0,120}data-height="1080"/);
21+
expect(minimal).toMatch(/window\.__timelines\["main"\]/);
1622
});
1723

1824
it("teaches check as the canonical quality gate", () => {

packages/cli/src/templates/_shared/AGENTS.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ Fix all errors before presenting the result. Warnings should be reviewed before
8888

8989
## Key Rules
9090

91-
1. Every timed element needs `data-start`, `data-duration`, and `data-track-index`
92-
2. Elements with timing **MUST** have `class="clip"`the framework uses this for visibility control
91+
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
92+
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
9393
3. Timelines must be paused and registered on `window.__timelines`:
9494
```js
9595
window.__timelines = window.__timelines || {};

packages/cli/src/templates/_shared/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ Fix all errors before presenting the result. Warnings should be reviewed before
8888

8989
## Key Rules
9090

91-
1. Every timed element needs `data-start`, `data-duration`, and `data-track-index`
92-
2. Elements with timing **MUST** have `class="clip"`the framework uses this for visibility control
91+
1. Every timed element needs `data-start` and a duration. `data-start` is what marks it as timed; `data-track-index` is an optional Studio display lane the render never reads
92+
2. Give timed visual elements `class="clip"`. The framework keys visibility off `data-start`, not the class, but the shared `.clip` CSS is what gives a scene its full-frame box, and `lint` warns without it
9393
3. Timelines must be paused and registered on `window.__timelines`:
9494
```js
9595
window.__timelines = window.__timelines || {};

skills-manifest.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"files": 138
77
},
88
"faceless-explainer": {
9-
"hash": "5de02925f2431645",
9+
"hash": "a7080101c30ceeb7",
1010
"files": 24
1111
},
1212
"figma": {
@@ -22,7 +22,7 @@
2222
"files": 17
2323
},
2424
"hyperframes-animation": {
25-
"hash": "67d23c9507ebb1e8",
25+
"hash": "a5be218c0b0fe377",
2626
"files": 121
2727
},
2828
"hyperframes-audio": {
@@ -34,7 +34,7 @@
3434
"files": 11
3535
},
3636
"hyperframes-core": {
37-
"hash": "74b9841f63c8f405",
37+
"hash": "cf5c75bf7dc59db6",
3838
"files": 20
3939
},
4040
"hyperframes-creative": {
@@ -62,11 +62,11 @@
6262
"files": 132
6363
},
6464
"pr-to-video": {
65-
"hash": "337d781bd7517d2c",
65+
"hash": "9ce0b5a7c75e969f",
6666
"files": 30
6767
},
6868
"product-launch-video": {
69-
"hash": "6d691b3db92e2c39",
69+
"hash": "d73fdea027509963",
7070
"files": 29
7171
},
7272
"remotion-to-hyperframes": {

skills/faceless-explainer/scripts/assemble-index.mjs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
// document order. Transitions are NOT written here — the transitions injector
1313
// mutates this file afterward (data-start/duration/track-index + GSAP).
1414
//
15-
// Track lanes (same-track time-overlap is illegal — lint timeline_track_too_dense):
15+
// Track lanes. Same-track time-overlap is this workflow's own assembly convention,
16+
// not a framework rule: the render never reads data-track-index, and no lint rule
17+
// checks overlap (timeline_track_too_dense counts elements per lane for readability).
18+
// The convention exists because the frame injector below ping-pongs 0/1 for overlaps:
1619
// 1 frame sub-comp clips (sequential; the injector 0/1-ping-pongs for overlaps)
1720
// 2 captions sub-comp clip (full-duration overlay, on top of frames)
1821
// 10 per-frame voice <audio>
@@ -215,7 +218,7 @@ function guardFrame(html, label) {
215218
for (let i = 1; i < list.length; i++) {
216219
if (list[i].start < list[i - 1].end - EPS) {
217220
errors.push(
218-
`${label}: clips on track ${track} overlap (one ends at ${r3(list[i - 1].end)}s, the next starts at ${r3(list[i].start)}s) — same-track time-overlap causes a render conflict. Put them on distinct data-track-index lanes or fix their windows.`,
221+
`${label}: clips on track ${track} overlap (one ends at ${r3(list[i - 1].end)}s, the next starts at ${r3(list[i].start)}s). This workflow's injector assumes one clip per lane at a time. The render itself tolerates the overlap; put them on distinct data-track-index lanes or fix their windows.`,
219222
);
220223
break; // one report per track is enough
221224
}

skills/faceless-explainer/scripts/transitions.mjs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,8 @@ function runVerify(argv) {
328328
a.start < b.start + b.duration - EPS && b.start < a.start + a.duration - EPS;
329329
const fail = [];
330330

331-
// (4) global no same-track overlap (the lint invariant).
331+
// (4) global no same-track overlap. This is this workflow's own lane convention,
332+
// not a lint rule: nothing in the framework rejects same-track overlap.
332333
const all = [...clips.values()];
333334
for (let i = 0; i < all.length; i++)
334335
for (let j = i + 1; j < all.length; j++) {

skills/hyperframes-animation/adapters/gsap.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ HyperFrames controls GSAP through its `gsap` runtime adapter. Create a paused ti
2727
- The registry key must match the composition root's `data-composition-id`.
2828
- Bracket and dot syntax both register: `window.__timelines["main"] = tl` and `window.__timelines.main = tl` are equivalent (the linter recognizes both). Bracket form is required when the id isn't a valid identifier (e.g. contains `-`).
2929
- Do not call `tl.play()` for render-critical motion.
30-
- Do not build timelines inside async code, timers, or event handlers.
30+
- Building inside an async callback such as `document.fonts.ready` is supported and common. What breaks is **registering the key before the build finishes**: an empty timeline registered early is treated as ready and nested empty, so it renders blank (`lint`: `gsap_timeline_registered_before_async_build`, error). Assign `window.__timelines[id] = tl` at the end of the callback. Do not drive render-critical motion from timers or event handlers.
3131
- Keep loops finite. HyperFrames renders finite video durations.
3232
- **Render duration comes from `data-duration` on the composition root, not from GSAP timeline length.** Do not pad the timeline with empty tweens like `tl.set({}, {}, 283)` to "extend" it. (Some external docs show this trick; in HyperFrames it conflicts with the seek-driven duration model — set `data-duration` instead.)
3333

@@ -70,10 +70,10 @@ HyperFrames is stricter than vanilla GSAP. Animate only:
7070

7171
**Forbidden** (breaks the renderer or the clip lifecycle):
7272

73-
- `display`, raw `visibility` never duration-tween these directly. Use `autoAlpha` (opacity plus endpoint visibility) or a zero-duration timeline set at an explicit boundary, and only on a non-clip element or wrapper inside a clip. Never target `.clip` itself.
73+
- `display`, raw `visibility` **on a clip element**: never duration-tween these. HyperFrames owns a clip's visibility and `lint` rejects it. Use `autoAlpha` (opacity plus endpoint visibility) or a zero-duration timeline set at an explicit boundary. Animating a clip element's other visual properties is fine and the shipped catalog does it throughout; what is forbidden is taking over its visibility.
7474
- Anything driven by `Math.random()`, `Date.now()`, `performance.now()`, or event handlers — animation state must be deterministic from time alone.
7575

76-
> **Note**: `docs/guides/gsap-animation.mdx` lists `width`/`height`/`visibility` in its "Supported Properties" — that list is too permissive for HyperFrames composition rules. This allowlist is the canonical one. See `hyperframes-core/references/determinism-rules.md` for the full deterministic-render contract.
76+
> **Note**: the list above is a **denylist**, not an allowlist. Properties outside it, including `width`, `height`, `filter`, `clipPath` and `strokeDashoffset`, are legitimate targets; prefer transforms and opacity where you have the choice, for performance rather than correctness. See `hyperframes-core/references/determinism-rules.md` for the full deterministic-render contract.
7777
7878
## References
7979

skills/hyperframes-animation/transitions/TRANSITION-REGISTRY.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ injector:
2323
2. Pulls `#el-<to>` wrapper `data-start` earlier by `duration_s` (creates the
2424
overlap window).
2525
3. Reassigns **all** clip `data-track-index` as a 0/1 ping-pong so the two
26-
overlapping wrappers never share a track (same-track overlap is illegal —
26+
overlapping wrappers never share a track (a readability convention, not a
27+
render constraint,
2728
`core/src/lint/rules/composition.ts`). Higher track composites on top.
2829
4. Stamps the `gsap_template` into `window.__timelines["main"]` at `T = overlap-start`.
2930

0 commit comments

Comments
 (0)