fix(skills): resolve the blueprint id from a qualified blueprint: field (#3337)

* fix(skills): resolve the blueprint id from a qualified `blueprint:` field

visual-design.md documents `blueprint:` as the id plus a `(Reproduce)` /
`(Adapt)` qualifier, and prints `dataviz-countup (Adapt)` as its worked example.
The packet builder used that raw field as a filename, so a qualified blueprint
looked for `<id> (Adapt).md`, found nothing, and inlined an empty string:
`selectedFile()` returns "" for a missing path. Every packet shipped without the
one document the frame was designed against, and the run still exited 0 with
nothing on stderr. `compose (Adapt)` missed the `compose` check the same way.

Parse the field into the id it names, once, so no caller resolves a raw field
value against the blueprints directory. A blueprint that resolves to no file is
now a named error rather than an empty section, matching how the builder already
treats a missing `src` and an oversize packet.

The existing tests only used bare ids, which is how the qualified form escaped;
they now cover both, and the missing-file case.

One owner: product-launch-video, faceless-explainer, pr-to-video and
general-video all delegate to frame-packets-core.mjs.

Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com>

* fix(skills): degrade, not fail, when the blueprints library is absent

Self-review catch on the previous commit. hyperframes-animation installs on
demand, so its blueprints/ directory can legitimately be missing — that is a
skill that isn't installed yet, not a frame naming a bad id. Throwing there
turned a silent degrade into a hard failure for a valid setup.

Distinguish the two: an absent blueprints/ warns and inlines nothing, exactly
as an absent rules/ already does in knownRuleIds; a present library that has no
file for this id still throws, because that is a typo or an unstripped
qualifier.

Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com>

* fix(skills): point two dead blueprint references at real shapes

CI surfaced these once an unresolvable blueprint stopped being silent. Both
named ids that have never existed in hyperframes-animation/blueprints/:

- faceless-explainer's frame template taught `messaging-multi-phase`, so an
  agent copying the template verbatim tagged a blueprint that resolves to
  nothing. dataviz-countup is what the same skill already uses in its own
  visual-design template and tests.
- pr-to-video's diff-excerpt guardrail fixture used `number-lockup`. The test is
  about diff excerpting and the id was incidental; the frame's own
  `counting-dynamic-scale` rule makes dataviz-countup the natural real shape.

A sweep of every `blueprint:` value across skills/ finds no others.

Co-Authored-By: anikam13 <22992075+anikam13@users.noreply.github.com>

---------

Co-authored-by: anikam13 <22992075+anikam13@users.noreply.github.com>
This commit is contained in:
Miguel Ángel
2026-08-20 16:37:29 -04:00
committed by GitHub
co-authored by anikam13
parent c66c9a4c76
commit d1482b0129
5 changed files with 116 additions and 14 deletions
@@ -83,14 +83,38 @@ export function citedRules(block, ruleIds) {
return [...new Set([...explicit, ...mentioned])].filter((id) => ruleIds.includes(id));
}
export function resourceSections(block, { animationDir, ruleIds }) {
// visual-design.md tells the author to write the blueprint as `<id> (Reproduce)`
// or `<id> (Adapt)` — the qualifier is direction for the frame worker, not part of
// the filename. Parse the field into the id it names (or null for `compose`), so
// no caller ever resolves a raw field value against the blueprints directory.
export function blueprintId(block) {
const raw = field(block, "blueprint");
if (!raw) return null;
const id = raw.replace(/\s*\([^)]*\)\s*$/, "").trim();
return id && id.toLowerCase() !== "compose" ? id : null;
}
export function resourceSections(block, { animationDir, ruleIds, frameId }) {
let sections = "";
const blueprint = field(block, "blueprint");
if (blueprint && blueprint.toLowerCase() !== "compose") {
sections += selectedFile(
join(animationDir, "blueprints", `${blueprint}.md`),
`Selected blueprint: ${blueprint}`,
);
const blueprint = blueprintId(block);
if (blueprint) {
const blueprintsDir = join(animationDir, "blueprints");
const path = join(blueprintsDir, `${blueprint}.md`);
// A blueprint that resolved to nothing used to inline an empty string, so the
// packet shipped without the one document the frame was designed against and
// the run still reported success. Name it instead — but only when the library
// is actually there to be named against. The animation skill installs on
// demand, so an absent blueprints/ is a missing install, not a bad id, and it
// degrades with a warning exactly like an absent rules/ (see knownRuleIds).
if (!existsSync(blueprintsDir)) {
console.warn(
`frame-packets: no blueprints dir at ${blueprintsDir} — packets will inline no blueprint`,
);
} else if (!existsSync(path)) {
throw new Error(`${frameId ?? "frame"}: blueprint "${blueprint}" has no file at ${path}`);
} else {
sections += selectedFile(path, `Selected blueprint: ${blueprint}`);
}
}
for (const rule of citedRules(block, ruleIds)) {
sections += selectedFile(
@@ -135,7 +159,7 @@ export function buildFramePackets({
const packets = frames.map((frame) => {
const id = frameId(frame);
if (validateFrame) validateFrame(frame, id);
const packet = `# Frame packet: ${id}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n${designTruthLine(projectDir)}\n- RULES_DIR: ${join(animationDir, "rules")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block, { animationDir, ruleIds })}${extraSections ? extraSections(frame.block) : ""}`;
const packet = `# Frame packet: ${id}\n\n## Project inputs\n\n- Project: ${resolve(projectDir)}\n${designTruthLine(projectDir)}\n- RULES_DIR: ${join(animationDir, "rules")}\n\n## Assigned storyboard block\n\n${frame.block}\n${resourceSections(frame.block, { animationDir, ruleIds, frameId: id })}${extraSections ? extraSections(frame.block) : ""}`;
const bytes = Buffer.byteLength(packet);
if (bytes > maxPacketBytes) {
throw new Error(`${id}: frame packet is ${bytes} bytes (limit ${maxPacketBytes})`);