fix(skills): extend transition roots without explicit duration (#2873)

* fix(skills): extend roots without explicit duration

fixes reported:1785307750.289819:transitions-extend-tail-root-duration-contract-mismatch; PR #2859 and unrelated claims remain unmodified.

* chore(skills): refresh manifest
This commit is contained in:
Miguel Ángel
2026-07-30 17:06:12 +02:00
committed by GitHub
parent 6c185f252f
commit 14ced90517
3 changed files with 77 additions and 7 deletions
+2 -2
View File
@@ -6,8 +6,8 @@
"files": 140
},
"faceless-explainer": {
"hash": "72acdcb31403531d",
"files": 23
"hash": "261a9740ec1378b0",
"files": 24
},
"figma": {
"hash": "517e4dc53c13ea05",
@@ -117,16 +117,18 @@ function extendFrameTail(hyperframesDir, frame, baseDuration, targetDuration, di
let extended = 0;
const rewritten = html.replace(/<([A-Za-z][\w:-]*)\b([^>]*)>/g, (tag, name, attrs) => {
const durationMatch = attrs.match(/\bdata-duration="([\d.]+)"/);
if (!durationMatch) return tag;
const duration = Number(durationMatch[1]);
if (!Number.isFinite(duration)) return tag;
const compositionMatch = attrs.match(/\bdata-composition-id="([^"]+)"/);
if (compositionMatch?.[1] === compId && !foundRoot) {
foundRoot = true;
return tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`);
return durationMatch
? tag.replace(/\bdata-duration="[\d.]+"/, `data-duration="${targetDuration}"`)
: tag.replace(/(\s*\/?>)$/, ` data-duration="${targetDuration}"$1`);
}
if (!durationMatch) return tag;
const duration = Number(durationMatch[1]);
if (!Number.isFinite(duration)) return tag;
if (name.toLowerCase() === "audio") return tag;
const startMatch = attrs.match(/\bdata-start="([\d.]+)"/);
if (!startMatch) return tag;
@@ -0,0 +1,68 @@
import assert from "node:assert/strict";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { spawnSync } from "node:child_process";
import test from "node:test";
const script = new URL("./transitions.mjs", import.meta.url).pathname;
function write(filePath, contents) {
mkdirSync(dirname(filePath), { recursive: true });
writeFileSync(filePath, contents);
}
test("inject extends a documented frame root that omits data-duration", (t) => {
const project = mkdtempSync(join(tmpdir(), "faceless-transitions-"));
t.after(() => rmSync(project, { force: true, recursive: true }));
write(
join(project, "STORYBOARD.md"),
`---
format: 1920x1080
---
## Frame 1 — First
- duration: 2s
- transition_in: cut
- status: animated
- src: compositions/frames/01-a.html
## Frame 2 — Second
- duration: 2s
- transition_in: crossfade
- status: animated
- src: compositions/frames/02-b.html
`,
);
write(
join(project, "index.html"),
`<!doctype html><html><body>
<div id="root" data-composition-id="main" data-duration="4"></div>
<div id="el-01-a" data-start="0" data-duration="2" data-track-index="0"></div>
<div id="el-02-b" data-start="2" data-duration="2" data-track-index="0"></div>
<script>window.__timelines={}; window.__timelines["main"] = gsap.timeline({ paused: true });</script>
</body></html>`,
);
write(
join(project, "compositions/frames/01-a.html"),
`<template><div id="root" data-composition-id="01-a"><div class="clip" data-start="0" data-duration="2" data-track-index="0">first</div></div></template>`,
);
write(
join(project, "compositions/frames/02-b.html"),
`<template><div id="root" data-composition-id="02-b"><div class="clip" data-start="0" data-duration="2" data-track-index="0">second</div></div></template>`,
);
const result = spawnSync(
process.execPath,
[script, "inject", "--storyboard", join(project, "STORYBOARD.md"), "--hyperframes", project],
{ encoding: "utf8" },
);
assert.equal(result.status, 0, result.stderr);
const outgoing = readFileSync(join(project, "compositions/frames/01-a.html"), "utf8");
assert.match(outgoing, /data-composition-id="01-a" data-duration="2.5"/);
assert.match(outgoing, /class="clip"[^>]*data-duration="2.5"/);
});