Files
hyperframes/scripts/creator-editing-recipes.test.mjs
T
Miguel Ángel 2db126d4c8 docs(skills): one owner doc for everyday edits and a hyperframes-studio skill (#4125)
* docs(skills): one owner doc for everyday edits and a hyperframes-studio skill

Agents got contradictory advice on volume fades and were blocked from splits
and copies by the Ask-agent guardrails.

- creator-editing-recipes.md is the single owner: adds align-to-event,
  copy-a-group and swap-media recipes and the one volume rule (use the lane)
- variables-and-media, data-attributes (skill and CLI copy), the gsap adapter
  and tracks-and-clips now defer to it
- Ask-agent guardrails allow the neighbour edits a split, retime, copy or swap
  requires and point at the owner doc
- new hyperframes-studio skill: sub-compositions for every scene, one caption
  track, one element kind per track, safe zones
- drift test lints every html example in the owner doc with the real linter
  and fails on any doc teaching a volume tween

* refactor(skills): drop unused tween allowlist from recipes drift test

* docs(skills): name the track kind attribute in the studio skill

* docs(skills): stop claiming lint rules that do not exist yet and fix track numbering

Review fixes:

- studio skill: drop the severity claim and the preview-overlay claim,
  group tracks by data-track-kind instead of numeric ranges that clashed
  with the recipes
- recipes: audio data-start is root time only in the root composition;
  scene-local inside a sub-composition file
- guardrails: timeline edits are exempt from the element-only and timing
  lines; the pointer names the skill reference, not a repo path
- montage doc defers to the volume lane; skill count is 21 everywhere
- drift test also scans the montage doc

* fix(skills): the ducking generator emits a volume lane and captions defer to the studio safe zones

- audio-duck.mjs prints a data-automation lane (clip-local times from the
  target data-start) instead of a timeline volume tween; --json adds the lane
- operations.md shows the generated lane; the drift test scans it
- embedded-captions safe-zone lines point at the studio skill, which owns
  the values

* chore(skills): regenerate the skills manifest after rebase

* docs(skills): remotion translation docs point volume ramps at the automation lane

* docs(skills): studio safe zones use the action-safe 90 and title-safe 80 boxes

* docs(skills): caption docs defer to the studio safe boxes

* chore(skills): regenerate the skills manifest after rebase

* fix(skills): studio skill names the safe margins; guardrail test matches the timeline-edit wording
2026-09-18 23:19:40 -04:00

76 lines
3.4 KiB
JavaScript

import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
import { lintHyperframeHtml } from "../packages/lint/src/index.ts";
const OWNER = "skills/hyperframes-core/references/creator-editing-recipes.md";
const STUDIO_SKILL = "skills/hyperframes-studio/SKILL.md";
const VOLUME_TWEEN = /\.(?:to|from|fromTo|set)\(\s*["'`]#[\w-]+["'`]\s*,\s*\{[^}]*\bvolume\s*:/;
const TWEEN_SCANNED = [
"skills/hyperframes-core/references/variables-and-media.md",
"skills/hyperframes-core/references/data-attributes.md",
"skills/hyperframes-animation/adapters/gsap.md",
"skills/hyperframes-audio/SKILL.md",
"skills/hyperframes-audio/references/attributes.md",
"skills/music-to-video/references/montage.md",
"skills/media-use/references/operations.md",
"packages/cli/src/docs/data-attributes.md",
];
const read = (path) => readFile(new URL(`../${path}`, import.meta.url), "utf8");
const htmlBlocks = (md) => [...md.matchAll(/```html\n([\s\S]*?)```/g)].map((m) => m[1]);
const wrap = (fragment) =>
fragment.includes("<html")
? fragment
: `<html><body><div id="root" data-composition-id="main" data-start="0" data-duration="120" data-width="1920" data-height="1080">${fragment}</div><script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script><script>window.__timelines = window.__timelines || {};</script></body></html>`;
test("every html example in the owner doc lints with no errors and no volume double-automation", async () => {
const blocks = htmlBlocks(await read(OWNER));
assert.ok(blocks.length >= 10, "expected the owner doc to keep its worked examples");
for (const [i, block] of blocks.entries()) {
const { findings } = await lintHyperframeHtml(wrap(block), { filePath: "index.html" });
const bad = findings.filter(
(f) => f.severity === "error" || f.code.startsWith("audio_volume_"),
);
assert.deepEqual(
bad.map((f) => `${f.code}: ${f.message}`),
[],
`example ${i + 1} in ${OWNER}`,
);
}
});
test("no other doc teaches a timeline tween as the way to fade volume", async () => {
for (const path of TWEEN_SCANNED) {
const lines = (await read(path)).split("\n");
const hit = lines.findIndex((l) => VOLUME_TWEEN.test(l));
assert.equal(hit, -1, `${path}:${hit + 1} teaches a volume tween; point at ${OWNER}`);
}
});
test("the Studio skill holds conventions only and points at the owner doc for edits", async () => {
const skill = await read(STUDIO_SKILL);
assert.match(skill, /creator-editing-recipes\.md/);
assert.equal(htmlBlocks(skill).length, 0, "a recipe restated in the Studio skill will drift");
});
test("the Remotion translation docs do not claim volume ramps are unsupported", async () => {
for (const path of [
"skills/remotion-to-hyperframes/references/limitations.md",
"skills/remotion-to-hyperframes/references/media.md",
]) {
assert.doesNotMatch(await read(path), /static `data-volume` only/, path);
}
});
test("the Studio skill's safe boxes equal the preview's", async () => {
const src = await read("packages/studio/src/utils/previewSafeMargins.ts");
const action = src.match(/ACTION_SAFE_PERCENT = (\d+)/)?.[1];
const title = src.match(/TITLE_SAFE_PERCENT = (\d+)/)?.[1];
const skill = await read(STUDIO_SKILL);
assert.match(skill, new RegExp(`Action-safe\\s*\\|\\s*${action}%`));
assert.match(skill, new RegExp(`Title-safe\\s*\\|\\s*${title}%`));
});