@litfamily/litopencode 1.0.28 → 1.0.30

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 (156) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README-Ko-KR.md +7 -7
  3. package/README.md +7 -7
  4. package/dist/activation-primary-prompts.js +15 -6
  5. package/dist/activation-prompt-utils.d.ts +2 -2
  6. package/dist/activation-prompt-utils.js +9 -3
  7. package/dist/activation-reentry.d.ts +29 -0
  8. package/dist/activation-reentry.js +71 -0
  9. package/dist/activation-routing.d.ts +1 -1
  10. package/dist/activation-routing.js +40 -7
  11. package/dist/activation-workflow-prompts.d.ts +2 -0
  12. package/dist/activation-workflow-prompts.js +32 -7
  13. package/dist/activation.d.ts +6 -1
  14. package/dist/activation.js +34 -7
  15. package/dist/cli/args.js +14 -0
  16. package/dist/cli/auto-update.js +2 -4
  17. package/dist/cli/command-plan.d.ts +40 -0
  18. package/dist/cli/command-plan.js +62 -0
  19. package/dist/cli/doctor.d.ts +4 -1
  20. package/dist/cli/doctor.js +65 -4
  21. package/dist/cli/host-detect.d.ts +58 -0
  22. package/dist/cli/host-detect.js +199 -0
  23. package/dist/cli/install-report.js +3 -1
  24. package/dist/cli/install.d.ts +9 -1
  25. package/dist/cli/install.js +134 -31
  26. package/dist/cli/managed-skill-assets.d.ts +1 -1
  27. package/dist/cli/managed-skill-assets.js +5 -3
  28. package/dist/cli/office-dependencies.d.ts +2 -0
  29. package/dist/cli/office-dependencies.js +7 -3
  30. package/dist/cli/scientific-visualization-dependencies.js +1 -1
  31. package/dist/cli/tui-config.d.ts +27 -2
  32. package/dist/cli/tui-config.js +77 -2
  33. package/dist/cli/types.d.ts +17 -0
  34. package/dist/cli.js +9 -6
  35. package/dist/commands.d.ts +10 -1
  36. package/dist/commands.js +17 -3
  37. package/dist/deliverable-hedge-guard.js +21 -9
  38. package/dist/features.d.ts +27 -1
  39. package/dist/features.js +40 -8
  40. package/dist/host/server.d.ts +7 -0
  41. package/dist/host/server.js +12 -0
  42. package/dist/host/tui.d.ts +7 -0
  43. package/dist/host/tui.js +12 -0
  44. package/dist/server.js +11 -1
  45. package/dist/skill-renames.d.ts +1 -1
  46. package/dist/skills.d.ts +9 -2
  47. package/dist/skills.js +22 -8
  48. package/dist/tui/mission-source-v2.d.ts +51 -0
  49. package/dist/tui/mission-source-v2.js +52 -0
  50. package/dist/v2/agents.d.ts +105 -0
  51. package/dist/v2/agents.js +185 -0
  52. package/dist/v2/commands.d.ts +44 -0
  53. package/dist/v2/commands.js +65 -0
  54. package/dist/v2/context.d.ts +67 -0
  55. package/dist/v2/context.js +17 -0
  56. package/dist/v2/events.d.ts +55 -0
  57. package/dist/v2/events.js +193 -0
  58. package/dist/v2/hooks.d.ts +39 -0
  59. package/dist/v2/hooks.js +61 -0
  60. package/dist/v2/server.d.ts +2 -0
  61. package/dist/v2/server.js +68 -0
  62. package/dist/v2/session-hooks.d.ts +64 -0
  63. package/dist/v2/session-hooks.js +230 -0
  64. package/dist/v2/tools.d.ts +26 -0
  65. package/dist/v2/tools.js +49 -0
  66. package/dist/v2/tui.d.ts +63 -0
  67. package/dist/v2/tui.js +259 -0
  68. package/docs/assets/cover-motion-still.webp +0 -0
  69. package/docs/assets/cover-motion.webp +0 -0
  70. package/docs/assets/cover.webp +0 -0
  71. package/docs/assets/readme/badge-version.svg +1 -1
  72. package/docs/assets/readme/litfamily-machines.png +0 -0
  73. package/docs/migration.md +4 -3
  74. package/docs/reference-Ko-KR.md +109 -7
  75. package/docs/reference.md +122 -9
  76. package/package.json +10 -5
  77. package/rules/bundled-rules/lit-humanizer.md +2 -0
  78. package/skills/deep-interview/SKILL.md +1 -1
  79. package/skills/lit-brainstorm/SKILL.md +299 -0
  80. package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
  81. package/skills/lit-brainstorm/references/lenses.md +106 -0
  82. package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
  83. package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
  84. package/skills/lit-docx/SKILL.md +1 -1
  85. package/skills/lit-docx/run.py +42 -4
  86. package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
  87. package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
  88. package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
  89. package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
  90. package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
  91. package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
  92. package/skills/lit-humanizer/SKILL.md +4 -0
  93. package/skills/lit-humanizer/scripts/core.mjs +81 -1
  94. package/skills/lit-humanizer/scripts/detect.mjs +4 -3
  95. package/skills/lit-humanizer/scripts/guard-scan.mjs +12 -6
  96. package/skills/lit-pptx/SKILL.md +1 -1
  97. package/skills/lit-pptx/command.mjs +94 -0
  98. package/skills/lit-pptx/run.mjs +16 -6
  99. package/skills/lit-pptx/scripts/compile-deck.js +2 -2
  100. package/skills/lit-typographic-motion/SKILL.md +5 -2
  101. package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
  102. package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
  103. package/skills/lit-typographic-motion/engine/command.mjs +94 -0
  104. package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
  105. package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
  106. package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
  107. package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
  108. package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
  109. package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
  110. package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
  111. package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
  112. package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
  113. package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
  114. package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
  115. package/skills/lit-typographic-motion/engine/look.mjs +113 -10
  116. package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
  117. package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
  118. package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
  119. package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
  120. package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
  121. package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
  122. package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
  123. package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
  124. package/skills/lit-typographic-motion/fonts/pins.json +3 -1
  125. package/skills/lit-typographic-motion/gate.mjs +34 -1
  126. package/skills/lit-typographic-motion/probe.mjs +8 -3
  127. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
  128. package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
  129. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
  130. package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
  131. package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
  132. package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
  133. package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
  134. package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
  135. package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
  136. package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
  137. package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
  138. package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
  139. package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
  140. package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
  141. package/skills/lit-typographic-motion/references/cover.md +423 -0
  142. package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
  143. package/skills/lit-typographic-motion/references/runtime.md +17 -2
  144. package/skills/lit-typographic-motion/references/stage.md +3 -0
  145. package/skills/lit-typographic-motion/references/treatment.md +4 -0
  146. package/skills/lit-typographic-motion/render.mjs +30 -6
  147. package/skills/lit-typographic-motion/runtime.mjs +12 -13
  148. package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
  149. package/skills/managed-skill-manifest.json +175 -31
  150. package/skills/review-work/SKILL.md +27 -18
  151. package/skills/skill-rename-aliases.json +2 -1
  152. package/tools/check-pack-payload.mjs +4 -1
  153. package/tools/payload-substance-allowlist.json +1 -1
  154. package/tools/payload-substance-parity.json +6 -4
  155. package/tools/qa-cover-recipes.mjs +167 -0
  156. package/tools/version-manifests.json +6 -0
@@ -7,6 +7,9 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
7
7
  import path from "node:path";
8
8
  import { downgrades, loadTreatment } from "./treatment.mjs";
9
9
  import { stageSha } from "./stage/scan.mjs";
10
+ import { COVER } from "./constants.mjs";
11
+ import { generatedAssetLabels } from "./cover/assets.mjs";
12
+ import { isCoverRun, validateCoverTreatment } from "./cover/treatment.mjs";
10
13
 
11
14
  export const LOOK_QUESTIONS = Object.freeze([
12
15
  "A stranger would say this film is for: <…>",
@@ -21,6 +24,18 @@ export const LOOK_QUESTIONS = Object.freeze([
21
24
  ]);
22
25
  // A "no" here means another round; for the others a "yes" does (Q7: a nameable gap).
23
26
  const noMeansRevise = new Set([1, 2, 3, 5, 6]);
27
+ // A cover is looked at for motion, the seam and damage, never for beats (references/cover.md).
28
+ export const COVER_LOOK_QUESTIONS = Object.freeze([
29
+ "Is something moving in every half-second window (the sheet's plot never lies flat on the red dashes)?",
30
+ "Does the seam disappear in the decoded file (seam.png, the right end of the plot)?",
31
+ "Does it still move after encoding, with no region that freezes and then pops?",
32
+ "Is the brand word present, legible and in place in every frame, never faded or swapped?",
33
+ "Are all three speed layers visible: a slow perpetual layer, a mid swell, beat events?",
34
+ "Does anything read as a slide, a card swap, a title fade or an end card, or does light wash in place instead of travelling as an edge?",
35
+ "Is any glyph, glint or glow damaged: a colour block, a dark disc, a visible box?",
36
+ "Would a skilled motion designer add or remove something? Name it."
37
+ ]);
38
+ const coverNoMeansRevise = new Set([1, 2, 3, 4, 5]);
24
39
  const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
25
40
  const shaFile = (file) => sha256(readFileSync(file));
26
41
  const readJson = (file, fallback = null) => (existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : fallback);
@@ -36,8 +51,31 @@ export function concreteObservation(text) {
36
51
  return words >= 4 || (words >= 2 && value.replace(/\s+/gu, "").length >= 12);
37
52
  }
38
53
 
39
- export function needsAnotherRound(answers) {
40
- return answers.filter((answer) => (noMeansRevise.has(answer.q) ? answer.verdict === "no" : answer.verdict === "yes")).map((answer) => answer.q);
54
+ export function needsAnotherRound(answers, kind = "film") {
55
+ const reviseOnNo = kind === "cover" ? coverNoMeansRevise : noMeansRevise;
56
+ return answers.filter((answer) => (reviseOnNo.has(answer.q) ? answer.verdict === "no" : answer.verdict === "yes")).map((answer) => answer.q);
57
+ }
58
+
59
+ // Cover defects, at most three per round, worst first: { t, region, frame, observed, cause, fix }.
60
+ // `t` is the loop time in seconds where it shows, `region` the [x, y, w, h] it covers in cover pixels,
61
+ // `frame` a file of the current stills set that shows it. A window render around t checks the fix.
62
+ function checkDefects(list, set) {
63
+ if (list === undefined) return [];
64
+ if (!Array.isArray(list)) throw new LookError("defects must be an array");
65
+ if (list.length > COVER.maxDefects) throw new LookError(`at most ${COVER.maxDefects} defects per round; record the worst first`);
66
+ const [width, height] = set.stills.size;
67
+ const loopSec = set.stills.durationSec;
68
+ return list.map((defect, i) => {
69
+ const at = `defect ${i + 1}`;
70
+ if (!defect || typeof defect !== "object") throw new LookError(`${at} must be an object`);
71
+ if (typeof defect.t !== "number" || !(defect.t >= 0) || defect.t >= loopSec) throw new LookError(`${at}: t must be the loop time in seconds (0 to ${loopSec}) where the defect shows`);
72
+ const r = defect.region;
73
+ if (!Array.isArray(r) || r.length !== 4 || !r.every(Number.isInteger) || r[0] < 0 || r[1] < 0 || r[2] <= 0 || r[3] <= 0 || r[0] + r[2] > width || r[1] + r[3] > height) throw new LookError(`${at}: region must be [x, y, w, h] inside the ${width}x${height} cover`);
74
+ if (!set.files.includes(defect.frame)) throw new LookError(`${at}: frame ${defect.frame} is not in the latest stills set`);
75
+ if (!concreteObservation(defect.observed)) throw new LookError(`${at}: observed must be a sentence naming what is visible there`);
76
+ for (const key of ["cause", "fix"]) if (typeof defect[key] !== "string" || defect[key].trim().length < 8 || /<[^<>\n]{1,80}>/u.test(defect[key])) throw new LookError(`${at}: ${key} must say something concrete`);
77
+ return { t: defect.t, region: r, frame: defect.frame, observed: defect.observed.trim(), cause: defect.cause.trim(), fix: defect.fix.trim() };
78
+ });
41
79
  }
42
80
 
43
81
  // The frames a round may cite: the latest stills set, plus the poster after a full render.
@@ -47,8 +85,12 @@ export function currentSet(out) {
47
85
  const stillsSha = shaFile(path.join(out, "stills", "stills.json"));
48
86
  const manifestFile = path.join(out, "manifest.json");
49
87
  const manifest = readJson(manifestFile);
50
- const film = manifest && manifest.stillsSha256 === stillsSha && manifest.craftRound === stills.round;
51
88
  const files = stills.files.map((entry) => entry.file);
89
+ if (stills.mode === "cover") {
90
+ const rendered = manifest?.mode === "cover" && manifest.stillsSha256 === stillsSha;
91
+ return { stills, kind: "cover", manifestSha: rendered ? shaFile(manifestFile) : stillsSha, files };
92
+ }
93
+ const film = manifest && manifest.stillsSha256 === stillsSha && manifest.craftRound === stills.round;
52
94
  const poster = ["poster.png", path.join("withheld", "poster.png")].find((name) => existsSync(path.join(out, name)));
53
95
  if (film && poster) files.push(poster);
54
96
  return { stills, kind: film ? "film" : "stills", manifestSha: film ? shaFile(manifestFile) : stillsSha, files };
@@ -64,6 +106,8 @@ export function recordLook(out, round, answersFile) {
64
106
  if (round !== expected) throw new LookError(`the next round is ${expected}; rounds share one counter with renders`);
65
107
  const set = currentSet(out);
66
108
  if (set.stills.round !== round) throw new LookError(`the latest stills set was rendered with --round ${set.stills.round}; look at the set of round ${round}`);
109
+ const cover = set.kind === "cover";
110
+ const questions = cover ? COVER_LOOK_QUESTIONS : LOOK_QUESTIONS;
67
111
  const blocked = input.blocked === "no-vision-tool";
68
112
  const viewed = [...new Set([...(Array.isArray(input.viewed) ? input.viewed : []), ...(Array.isArray(input.answers) ? input.answers.map((answer) => answer?.frame).filter((frame) => frame && frame !== "sound-cues.json") : [])])];
69
113
  const unknown = viewed.filter((frame) => typeof frame !== "string" || !set.files.includes(frame.replace(/^\.\//u, "")));
@@ -71,24 +115,31 @@ export function recordLook(out, round, answersFile) {
71
115
  const answers = Array.isArray(input.answers) ? input.answers : [];
72
116
  if (!blocked) {
73
117
  const asked = new Set(answers.map((answer) => answer?.q));
74
- const missing = LOOK_QUESTIONS.map((_, i) => i + 1).filter((q) => !asked.has(q));
118
+ const missing = questions.map((_, i) => i + 1).filter((q) => !asked.has(q));
75
119
  if (missing.length) throw new LookError(`answer every question; missing ${missing.join(", ")}`);
76
120
  for (const answer of answers) {
77
- if (!Number.isInteger(answer.q) || answer.q < 1 || answer.q > 9) throw new LookError(`unknown question ${answer.q}`);
121
+ if (!Number.isInteger(answer.q) || answer.q < 1 || answer.q > questions.length) throw new LookError(`unknown question ${answer.q}`);
78
122
  if (!["yes", "no"].includes(answer.verdict)) throw new LookError(`question ${answer.q}: verdict must be "yes" or "no"`);
79
123
  if (!concreteObservation(answer.observed)) throw new LookError(`question ${answer.q}: "observed" must be a sentence naming something visible in ${answer.frame ?? "the frame"}, not a bare answer`);
80
- if (answer.q === 6 ? !["sound-cues.json", ...set.files].includes(answer.frame) : !set.files.includes(answer.frame)) throw new LookError(`question ${answer.q}: frame ${answer.frame} is not in the latest stills set`);
81
- if (answer.q === 1 && !["blind", "self"].includes(answer.by)) throw new LookError('question 1: "by" must be "blind" (a fresh subagent) or "self"');
124
+ if (answer.q === 6 && !cover ? !["sound-cues.json", ...set.files].includes(answer.frame) : !set.files.includes(answer.frame)) throw new LookError(`question ${answer.q}: frame ${answer.frame} is not in the latest stills set`);
125
+ if (answer.q === 1 && !cover && !["blind", "self"].includes(answer.by)) throw new LookError('question 1: "by" must be "blind" (a fresh subagent) or "self"');
82
126
  }
83
127
  }
84
- if (round === 1 && set.kind !== "stills") throw new LookError("round 1 is a stills round, before the first full render");
85
- if (round === 1 && !blocked && (typeof input.change !== "string" || !input.change.trim() || typeof input.weakestBeat !== "string" || !input.weakestBeat.trim())) throw new LookError("round 1 must name the weakest beat and the change you will make");
128
+ if (cover) {
129
+ if (round === 1 && !blocked && (typeof input.change !== "string" || !input.change.trim())) throw new LookError("round 1 of a cover must name the change you will make before round 2");
130
+ } else {
131
+ if (input.defects !== undefined) throw new LookError("defects belong to a cover round; a film round names its weakest beat");
132
+ if (round === 1 && set.kind !== "stills") throw new LookError("round 1 is a stills round, before the first full render");
133
+ if (round === 1 && !blocked && (typeof input.change !== "string" || !input.change.trim() || typeof input.weakestBeat !== "string" || !input.weakestBeat.trim())) throw new LookError("round 1 must name the weakest beat and the change you will make");
134
+ }
135
+ const defects = cover ? checkDefects(input.defects, set) : [];
86
136
  const frames = Object.fromEntries(viewed.map((frame) => [frame, shaFile(path.join(out, frame))]));
87
137
  const entry = {
88
138
  round, kind: set.kind, manifestSha256: set.manifestSha, frames, answers: answers.map(({ q, verdict, frame, observed, by }) => ({ q, verdict, frame, observed, ...(by ? { by } : {}) })),
89
139
  ...(input.weakestBeat ? { weakestBeat: String(input.weakestBeat) } : {}), ...(input.change ? { change: String(input.change) } : {}),
90
140
  ...(Array.isArray(input.aids) ? { aids: input.aids.map(String) } : {}), ...(blocked ? { blocked: "no-vision-tool" } : {}),
91
- needsAnotherRound: blocked ? [] : needsAnotherRound(answers), host: process.env.OPENCODE === "1" ? "opencode" : null, recordedAt: new Date().toISOString()
141
+ ...(cover ? { defects } : {}),
142
+ needsAnotherRound: blocked ? [] : needsAnotherRound(answers, set.kind), host: process.env.OPENCODE === "1" ? "opencode" : null, recordedAt: new Date().toISOString()
92
143
  };
93
144
  look.rounds.push(entry);
94
145
  writeFileSync(path.join(out, "look.json"), JSON.stringify(look, null, 2) + "\n");
@@ -106,9 +157,60 @@ function imageReads(out) {
106
157
  return readFileSync(file, "utf8").split("\n").filter(Boolean).flatMap((line) => { try { return [JSON.parse(line)]; } catch { return []; } });
107
158
  }
108
159
 
160
+ // The cover branch of the done check (references/cover.md, "Rounds and done"): a valid cover
161
+ // treatment; the last render's report at PASS+TARGET on the decoded file, with nothing changed since;
162
+ // two look rounds, the first naming its change, the last on the final render's stills and opening
163
+ // the sheet, the poster and the seam pair with the read tool. Generated assets owe a reply label.
164
+ export function coverDoneState(out) {
165
+ const reasons = [], open = [];
166
+ const labels = generatedAssetLabels(out);
167
+ const result = (status, code) => ({ status, code, done: code === 0, reasons, open, downgraded: [], labels, mode: "cover" });
168
+ let raw = null;
169
+ try { raw = JSON.parse(readFileSync(path.join(out, "treatment.json"), "utf8")); } catch (error) { reasons.push(`treatment.json is unreadable (${error.message})`); return result("NOT DONE", 1); }
170
+ const valid = validateCoverTreatment(raw);
171
+ if (!valid.ok) { reasons.push(`BLOCKED_TREATMENT_INVALID (16): field ${valid.field}: ${valid.detail}`); return result("NOT DONE", 1); }
172
+ const manifestFile = path.join(out, "manifest.json");
173
+ const manifest = readJson(manifestFile);
174
+ const report = existsSync(path.join(out, "gate-report.txt")) ? readFileSync(path.join(out, "gate-report.txt"), "utf8") : null;
175
+ if (manifest?.mode !== "cover" || !report) { reasons.push("no finished cover render (manifest.json or gate-report.txt is missing): run render.mjs cover"); return result("NOT DONE", 1); }
176
+ if (manifest.treatmentSha256 !== shaFile(path.join(out, "treatment.json"))) reasons.push("the treatment changed after the last cover render; render again");
177
+ if (existsSync(path.join(out, "cover")) && manifest.coverSha256 !== stageSha(path.join(out, "cover"))) reasons.push("the cover page or an asset changed after the last cover render; render again");
178
+ const passed = /^COVER gate --strict: PASS\+TARGET$/mu.test(report);
179
+ const rounds = readJson(path.join(out, "look.json"), { rounds: [] }).rounds;
180
+ const first = rounds[0], last = rounds.at(-1);
181
+ const round = manifest.craftRound ?? last?.round ?? 0;
182
+ if (!passed) (round >= 3 ? open : reasons).push(`the decoded cover did not reach every TARGET gate (${report.split("\n").filter((line) => /^\s+\S+ (?:FAIL|pass\(floor\))(?: |$)/u.test(line)).map((line) => line.trim().split(" ")[0]).join(", ") || "see gate-report.txt"})`);
183
+ if (existsSync(path.join(out, "withheld"))) reasons.push("the flash audit failed; the cover is withheld");
184
+ if (rounds.length < 2) reasons.push(`${rounds.length} look round(s) recorded; a cover needs two: look, make the named change, render again, look again`);
185
+ if (first && !first.blocked && !first.change) reasons.push("round 1 must name the change made before round 2");
186
+ let unviewed = false;
187
+ if (last) {
188
+ if (last.kind !== "cover" || last.manifestSha256 !== shaFile(manifestFile)) reasons.push("the last look round was not taken on the final cover render; look again at this render");
189
+ let set = null;
190
+ try { set = currentSet(out); } catch (error) { reasons.push(error.message); }
191
+ if (set && !last.blocked) {
192
+ const required = set.stills.files.filter((entry) => ["sheet", "poster", "seam"].includes(entry.kind)).map((entry) => entry.file);
193
+ const skipped = required.filter((file) => !Object.hasOwn(last.frames ?? {}, file));
194
+ if (skipped.length) reasons.push(`the last round did not view: ${skipped.join(", ")}`);
195
+ }
196
+ if (last.blocked === "no-vision-tool") unviewed = true;
197
+ if (last.host === "opencode" && !last.blocked) {
198
+ const reads = imageReads(out);
199
+ const notRead = Object.entries(last.frames ?? {}).filter(([file, sha]) => !reads.some((read) => read.file === file && read.sha256 === sha)).map(([file]) => file);
200
+ if (notRead.length) reasons.push(`not opened with the read tool: ${notRead.join(", ")}`);
201
+ }
202
+ if (last.needsAnotherRound?.length) (last.round >= 3 ? open : reasons).push(`cover look questions ${last.needsAnotherRound.join(", ")} ask for another round`);
203
+ }
204
+ if (reasons.length) return result("NOT DONE", 1);
205
+ if (unviewed) return result("DONE_UNVIEWED", 3);
206
+ if (open.length) return result("DONE_WITH_OPEN_ITEMS", 2);
207
+ return result("DONE", 0);
208
+ }
209
+
109
210
  // Status: DONE (0), NOT DONE (1), DONE_WITH_OPEN_ITEMS (2, round 3 spent), DONE_UNVIEWED (3).
110
211
  export function doneState(dir) {
111
212
  const out = path.resolve(dir);
213
+ if (isCoverRun(out)) return coverDoneState(out);
112
214
  const reasons = [], open = [];
113
215
  const result = (status, code) => ({ status, code, done: code === 0, reasons, open, downgraded });
114
216
  let treatment = null, downgraded = [];
@@ -161,6 +263,7 @@ export function formatDone(state) {
161
263
  for (const reason of state.reasons.slice(1)) lines.push(`- ${reason}`);
162
264
  for (const item of state.open) lines.push(`open: ${item}`);
163
265
  for (const item of state.downgraded) lines.push(`downgraded: ${item}`);
266
+ if (state.done || state.code === 2 || state.code === 3) for (const label of state.labels ?? []) lines.push(label);
164
267
  if (state.status === "DONE_UNVIEWED") lines.push("nobody viewed the frames: say so plainly in the reply");
165
268
  return lines.join("\n");
166
269
  }
@@ -243,7 +243,7 @@ export function muxTrack(videoFile, track, durationSec, target) {
243
243
  const result = spawnSync("ffmpeg", ["-y", "-loglevel", "error", "-i", videoFile, "-i", track.file, "-map", "0:v:0", "-map", "1:a:0", "-c:v", "copy",
244
244
  "-af", `aresample=${RATE},apad,atrim=0:${durationSec.toFixed(6)},afade=t=in:d=${fadeSec},afade=t=out:st=${fadeOut}:d=${fadeSec}`,
245
245
  "-ac", "2", "-c:a", "aac", "-b:a", "256k", "-movflags", "+faststart", target], { encoding: "utf8", timeout: 300000 });
246
- if (result.status !== 0) throw Object.assign(new Error(`SOUND_INVALID (20): the ${track.mode} track could not be muxed: ${(result.stderr || "").trim().split("\n").at(-1)}`), { exitCode: SOUND_EXIT });
246
+ if (result.status !== 0) throw Object.assign(new Error(`SOUND_INVALID (20): the ${track.mode} track could not be muxed: ${(result.stderr || "").trim().split(/\r?\n/u).at(-1)}`), { exitCode: SOUND_EXIT });
247
247
  }
248
248
 
249
249
  // Decodes a film's audio stream (48 kHz stereo float) for the gate; null when there is none.
@@ -46,32 +46,20 @@ export function stageFonts(skillRoot, state, pins) {
46
46
  return { css, files, families: [...new Set(faces.map((face) => face.family))] };
47
47
  }
48
48
 
49
- // Launch one stage browser. `profile` is a short directory under the run's .run/, removed on close.
50
- export async function openStage(chromium, { chromePath, profile, stageDir, width, height, fps, seed, fonts, extraInit }) {
51
- rmSync(profile, { recursive: true, force: true });
52
- mkdirSync(profile, { recursive: true });
53
- const flags = stageChromeFlags(width, height);
54
- let context;
55
- try {
56
- context = await chromium.launchPersistentContext(profile, {
57
- executablePath: chromePath, headless: true, args: flags, chromiumSandbox: true, timeout: 30000,
58
- viewport: { width, height }, deviceScaleFactor: 1, serviceWorkers: "block"
59
- });
60
- } catch (error) {
61
- rmSync(profile, { recursive: true, force: true });
62
- const line = String(error?.message ?? error).split("\n").find((part) => part.trim()) ?? String(error);
63
- throw Object.assign(new Error(`BLOCKED_NO_CHROME (10): Chrome failed to launch headless: ${line}; fix: check that Chrome can start headless in this sandbox`), { exitCode: 10 });
64
- }
49
+ // Serve the stage origin from disk through request interception: the product's scripts and faces
50
+ // under /lit/, every other path from inside `stageDir` only. Anything else is recorded, never fetched:
51
+ // another origin in `blocked`, a path that leaves the directory or has no stage type in `refused`, a
52
+ // file that is not there in `missing`. The stage and the cover browsers both use it.
53
+ export async function serveStage(context, { stageDir, fonts, scripts }) {
65
54
  const blocked = [];
66
55
  const refused = [];
67
56
  const missing = [];
68
57
  const root = realpathSync(stageDir);
69
- const kit = kitSource();
70
58
  await context.route("**/*", async (route) => {
71
59
  const url = new URL(route.request().url());
72
60
  if (url.origin !== stageOrigin) { blocked.push(url.href); return route.abort("blockedbyclient"); }
73
61
  const pathname = decodeURIComponent(url.pathname);
74
- if (pathname === "/lit/stage-kit.js") return route.fulfill({ status: 200, contentType: contentTypes[".js"], body: kit });
62
+ if (Object.hasOwn(scripts, pathname)) return route.fulfill({ status: 200, contentType: contentTypes[".js"], body: scripts[pathname] });
75
63
  if (pathname === "/lit/fonts.css") return route.fulfill({ status: 200, contentType: contentTypes[".css"], body: fonts.css });
76
64
  if (pathname.startsWith("/lit/fonts/")) {
77
65
  const file = fonts.files.get(pathname.slice("/lit/fonts/".length));
@@ -87,6 +75,26 @@ export async function openStage(chromium, { chromePath, profile, stageDir, width
87
75
  if (!type) { refused.push(rel); return route.fulfill({ status: 404, body: "" }); }
88
76
  return route.fulfill({ status: 200, contentType: type, body: readFileSync(real) });
89
77
  });
78
+ return { blocked, refused, missing };
79
+ }
80
+
81
+ // Launch one stage browser. `profile` is a short directory under the run's .run/, removed on close.
82
+ export async function openStage(chromium, { chromePath, profile, stageDir, width, height, fps, seed, fonts, extraInit }) {
83
+ rmSync(profile, { recursive: true, force: true });
84
+ mkdirSync(profile, { recursive: true });
85
+ const flags = stageChromeFlags(width, height);
86
+ let context;
87
+ try {
88
+ context = await chromium.launchPersistentContext(profile, {
89
+ executablePath: chromePath, headless: true, args: flags, chromiumSandbox: true, timeout: 30000,
90
+ viewport: { width, height }, deviceScaleFactor: 1, serviceWorkers: "block"
91
+ });
92
+ } catch (error) {
93
+ rmSync(profile, { recursive: true, force: true });
94
+ const line = String(error?.message ?? error).split("\n").find((part) => part.trim()) ?? String(error);
95
+ throw Object.assign(new Error(`BLOCKED_NO_CHROME (10): Chrome failed to launch headless: ${line}; fix: check that Chrome can start headless in this sandbox`), { exitCode: 10 });
96
+ }
97
+ const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource() } });
90
98
  await context.addInitScript({ content: `window.__litStageConfig = ${JSON.stringify({ seed: seed >>> 0, fps })};\n${clockSource()}` });
91
99
  if (extraInit) await context.addInitScript({ content: extraInit });
92
100
  const page = context.pages()[0] ?? await context.newPage();
@@ -0,0 +1,38 @@
1
+ // Cover init: injected into a cover page before any of its own scripts (engine/cover/capture.mjs).
2
+ // A cover frame is a pure function of loop time t, and the renderer asks for frames out of order,
3
+ // so every wall clock, frame loop and unseeded entropy source answers with an error instead of a
4
+ // value. Each refusal is remembered by name, because a page may swallow the error in a try block;
5
+ // the renderer reads the list after every frame and stops with exit 18 naming the call. CSS
6
+ // animations and transitions would run on the real clock, so they are switched off.
7
+ (function () {
8
+ "use strict";
9
+ const used = [];
10
+ const refuse = (name, instead) => function refused() {
11
+ if (!used.includes(name)) used.push(name);
12
+ throw new Error(`${name} is not available in a cover page: ${instead}`);
13
+ };
14
+ const fromT = "draw every frame from t";
15
+ const RealDate = Date;
16
+ function CoverDate(...args) {
17
+ if (new.target && args.length > 0) return new RealDate(...args);
18
+ return refuse("Date", fromT)();
19
+ }
20
+ CoverDate.prototype = RealDate.prototype;
21
+ CoverDate.now = refuse("Date.now", fromT);
22
+ CoverDate.parse = RealDate.parse;
23
+ CoverDate.UTC = RealDate.UTC;
24
+ Object.defineProperty(window, "Date", { value: CoverDate, writable: true, configurable: true });
25
+ Object.defineProperty(Performance.prototype, "now", { value: refuse("performance.now", fromT), writable: true, configurable: true });
26
+ Math.random = refuse("Math.random", "use LitLoop.rng(seed) for layout and t for motion");
27
+ window.requestAnimationFrame = refuse("requestAnimationFrame", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
28
+ window.setInterval = refuse("setInterval", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
29
+ if (window.crypto) Object.defineProperty(window.crypto, "getRandomValues", { value: refuse("crypto.getRandomValues", "use LitLoop.rng(seed)"), configurable: true });
30
+ const still = () => {
31
+ const style = document.createElement("style");
32
+ style.textContent = "*, *::before, *::after { animation: none !important; transition: none !important; }";
33
+ (document.head ?? document.documentElement).appendChild(style);
34
+ };
35
+ if (document.documentElement) still();
36
+ else document.addEventListener("readystatechange", still, { once: true });
37
+ window.__litCover = Object.freeze({ used: () => used.slice() });
38
+ })();
@@ -0,0 +1,102 @@
1
+ // LitLoop, served at /lit/loop-kit.js to a cover page (references/cover.md). A cover page draws one
2
+ // picture per loop time t in [0, 1) through window.renderFrame(t); the cover renderer seeks the
3
+ // frames t = i / N in any order, more than once, in fresh browsers. So every helper below is either
4
+ // periodic in t (its value at t = 1 is its value at t = 0) or a pure function of its arguments, and
5
+ // none of them keeps state between calls. Randomness exists only as rng(seed), for layout.
6
+ (function (scope) {
7
+ "use strict";
8
+
9
+ const TAU = 2 * Math.PI;
10
+ const fract = (x) => x - Math.floor(x);
11
+ const clamp = (x, lo = 0, hi = 1) => Math.min(hi, Math.max(lo, x));
12
+ const mix = (a, b, k) => a * (1 - k) + b * k;
13
+
14
+ // mulberry32: the same seed always lays out the same picture.
15
+ function rng(seed) {
16
+ let state = seed >>> 0;
17
+ return function next() {
18
+ state = (state + 0x6d2b79f5) >>> 0;
19
+ let z = state;
20
+ z = Math.imul(z ^ (z >>> 15), z | 1);
21
+ z ^= z + Math.imul(z ^ (z >>> 7), z | 61);
22
+ return ((z ^ (z >>> 14)) >>> 0) / 4294967296;
23
+ };
24
+ }
25
+
26
+ // Value noise on a seeded 3D lattice, smooth-stepped between lattice points.
27
+ function makeNoise(seed) {
28
+ const draw = rng(seed);
29
+ const table = new Float32Array(256).map(() => draw() * 2 - 1);
30
+ const order = Array.from({ length: 256 }, (_, i) => i);
31
+ for (let i = 255; i > 0; i--) {
32
+ const j = Math.floor(draw() * (i + 1));
33
+ const keep = order[i];
34
+ order[i] = order[j];
35
+ order[j] = keep;
36
+ }
37
+ const hash = (x, y, z) => table[order[(order[(order[x & 255] + y) & 255] + z) & 255]];
38
+ const fade = (k) => k * k * (3 - 2 * k);
39
+ return function noise(x, y, z) {
40
+ const ix = Math.floor(x), iy = Math.floor(y), iz = Math.floor(z);
41
+ const u = fade(x - ix), v = fade(y - iy), w = fade(z - iz);
42
+ const plane = (zz) => mix(
43
+ mix(hash(ix, iy, zz), hash(ix + 1, iy, zz), u),
44
+ mix(hash(ix, iy + 1, zz), hash(ix + 1, iy + 1, zz), u),
45
+ v
46
+ );
47
+ return mix(plane(iz), plane(iz + 1), w);
48
+ };
49
+ }
50
+
51
+ // Noise sampled on a circle of `radius` around `offset`: one lap per loop, so it closes at t = 1.
52
+ // Different offsets give different copies of the same wander.
53
+ function loopNoise(noise, t, offset = 0, radius = 1.25) {
54
+ const a = TAU * t;
55
+ return noise(offset + radius * Math.cos(a), radius * Math.sin(a), 0.41 * offset);
56
+ }
57
+
58
+ // Easing shapes for events. Motion that never stops (a drift, a lap) stays linear.
59
+ const ease = Object.freeze({
60
+ smooth: (p) => p * p * (3 - 2 * p),
61
+ outCubic: (p) => 1 - Math.pow(1 - p, 3),
62
+ inCubic: (p) => p * p * p,
63
+ outExpo: (p) => (p >= 1 ? 1 : 1 - Math.pow(2, -10 * p)),
64
+ outBack: (p, s = 1.6) => 1 + (s + 1) * Math.pow(p - 1, 3) + s * Math.pow(p - 1, 2),
65
+ gain: (p, k) => (p < 0.5 ? 0.5 * Math.pow(2 * p, k) : 1 - 0.5 * Math.pow(2 - 2 * p, k))
66
+ });
67
+
68
+ // A symmetric bump of half-width w centred on c; 0 outside it. Micro events use this: an
69
+ // exponential tail is a slow fade, and the lossy encoder drops slow fades.
70
+ function cubicPulse(c, w, x) {
71
+ const d = Math.abs(x - c) / w;
72
+ return d >= 1 ? 0 : 1 - d * d * (3 - 2 * d);
73
+ }
74
+ // A fast attack and an exponential decay; kept for shapes that end before the loop does.
75
+ function expImpulse(x, k) {
76
+ const h = k * x;
77
+ return h * Math.exp(1 - h);
78
+ }
79
+
80
+ // The loop position of one copy: `cycles` laps per loop, shifted back by `offset`.
81
+ const phase = (t, offset = 0, cycles = 1) => fract(cycles * t - offset);
82
+ // How far p is from c around the loop (0 to 0.5): an event can straddle t = 0.
83
+ function cdist(p, c) {
84
+ const d = fract(p - c);
85
+ return d > 0.5 ? 1 - d : d;
86
+ }
87
+ // A one-shot inside the loop: progress 0..1 while p is in [start, start + dur) (wrapping), else -1.
88
+ function windowAt(p, start, dur) {
89
+ const q = fract(p - start);
90
+ return q < dur ? q / dur : -1;
91
+ }
92
+ // Dwell and move: `stops` poses per lap, travelling during the last `travel` share of each slot.
93
+ function steps(p, stops, travel = 0.35, shape = ease.smooth) {
94
+ const slot = Math.floor(p * stops);
95
+ const inSlot = p * stops - slot;
96
+ return (slot + shape(clamp((inSlot - (1 - travel)) / travel))) / stops;
97
+ }
98
+ // 0 -> 1 -> 0 over one lap, resting at both ends.
99
+ const swell = (p) => 0.5 * (1 - Math.cos(TAU * p));
100
+
101
+ scope.LitLoop = Object.freeze({ TAU, clamp, fract, mix, rng, makeNoise, loopNoise, ease, cubicPulse, expImpulse, phase, cdist, window: windowAt, steps, swell });
102
+ })(typeof window === "undefined" ? globalThis : window);
@@ -402,15 +402,15 @@ export async function renderStage(out, { round = 1, stillsOnly = false } = {}) {
402
402
  }
403
403
  }
404
404
 
405
- // `render.mjs stage [--detach]`: a detached run writes its log and exit code under .run/ so a host
406
- // tool timeout never kills a long render.
407
- export function detachStage(out, args) {
405
+ // `render.mjs stage|cover [--detach]`: a detached run writes its log and exit code under .run/ so a
406
+ // host tool timeout never kills a long render.
407
+ export function detachStage(out, args, mode = "stage") {
408
408
  const runDir = path.join(out, ".run");
409
409
  mkdirSync(runDir, { recursive: true });
410
410
  const logFile = path.join(runDir, "render.log");
411
411
  rmSync(path.join(runDir, "render-exit.json"), { force: true });
412
412
  const fd = openSync(logFile, "w");
413
- const child = spawn(process.execPath, [path.join(skillRoot, "render.mjs"), "stage", ...args.filter((arg) => arg !== "--detach"), "--exit-file", path.join(runDir, "render-exit.json")], { detached: true, stdio: ["ignore", fd, fd] });
413
+ const child = spawn(process.execPath, [path.join(skillRoot, "render.mjs"), mode, ...args.filter((arg) => arg !== "--detach"), "--exit-file", path.join(runDir, "render-exit.json")], { detached: true, stdio: ["ignore", fd, fd] });
414
414
  child.unref();
415
415
  closeSync(fd);
416
416
  return { pid: child.pid, logFile, progressFile: path.join(runDir, "progress.json"), exitFile: path.join(runDir, "render-exit.json") };
@@ -229,6 +229,7 @@ export function loadTreatment(out) {
229
229
  if (!existsSync(file)) throw fail("treatment", `no treatment.json in ${out}; write it first (references/treatment.md)`);
230
230
  let parsed;
231
231
  try { parsed = JSON.parse(readFileSync(file, "utf8")); } catch (error) { throw fail("treatment", `treatment.json is not JSON (${error.message})`); }
232
+ if (parsed?.mode === "cover") throw fail("mode", "this is a cover treatment; render it with `render.mjs cover --out <dir>` (references/cover.md)");
232
233
  const result = validateTreatment(parsed);
233
234
  if (!result.ok) throw fail(result.field, result.detail);
234
235
  return { treatment: parsed, file };
@@ -0,0 +1,93 @@
1
+ Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono) JetBrainsMono-Italic[wght].ttf: Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -39,6 +39,8 @@
39
39
  "fonts/stroke/EMSReadability.svg": "eebece4f2a47b9d3a8f40cab2b0f689ca60902997abd5c81064ef0d2a3a05522",
40
40
  "fonts/stroke/EMSTech.svg": "7b5a420d4e16285213e7e2b1d187d086835c2dd7a7f64c5b8057434f082fc46f",
41
41
  "fonts/stroke/OFL.txt": "f19c674290e5dc79f02e8efe16139ab56a06a7128876f5b1579ffd0db5bc897e",
42
- "fonts/stroke/CREDITS": "7be8857db66d53b6c6a7b82465a522cb4bbd1533af40f3e02f7dd7ffc9d44f9a"
42
+ "fonts/stroke/CREDITS": "7be8857db66d53b6c6a7b82465a522cb4bbd1533af40f3e02f7dd7ffc9d44f9a",
43
+ "fonts/JetBrainsMono-wght-latin.woff2": "18be452724bfdc236c074ca94a249a7f41a86752c7d04ab258ce9ed5651f6a7e",
44
+ "fonts/JetBrainsMono-OFL.txt": "403581b69dac5cff4079205e01c6b467e56af449ecbd7247693ddb1baafa005b"
43
45
  }
44
46
  }
@@ -17,6 +17,7 @@ import { pinnedSampleTimes } from "./engine/timing.mjs";
17
17
  import { checkStageRun, formatStageReport } from "./engine/stage/gate.mjs";
18
18
  import { soundChecks } from "./engine/sound.mjs";
19
19
  import { doneState, formatDone } from "./engine/look.mjs";
20
+ import { coverGate, formatCoverReport } from "./engine/cover/gate.mjs";
20
21
 
21
22
  export const exportNames = ["film.mp4", "preview.webp", "preview.gif", "poster.png", "reduced-motion.png"];
22
23
  const isShot = (entry) => entry.kind === "line" || entry.kind === "scene";
@@ -569,9 +570,29 @@ export function completionState(dir) {
569
570
  return doneState(dir);
570
571
  }
571
572
 
573
+ // `--cover <file>`: the cover gate on any encoded cover (.webp, .gif) or PNG frame folder. With the
574
+ // capture folders it adds G0, G5 and G6; `--strict` exits 13 unless every gate reaches TARGET.
575
+ // A cover output directory re-gates its own cover.webp with the folders its last render kept.
576
+ export function runCoverGate(input, { frames, replay, masks, title, displayWidth, strict = false } = {}) {
577
+ const box = typeof title === "string" ? title.split(",").map(Number) : title;
578
+ const result = coverGate(input, { frames, replay, masks, title: box, glyph: box && masks ? { displayWidth } : null });
579
+ const report = formatCoverReport(result, { strict });
580
+ return { result, report, exitCode: result.pass && (!strict || result.target) ? EXIT.OK : EXIT.GATE_FAIL_QA };
581
+ }
582
+
583
+ function coverDirGate(root) {
584
+ const manifest = JSON.parse(readFileSync(path.join(root, "manifest.json"), "utf8"));
585
+ const file = [path.join(root, manifest.file), path.join(root, "withheld", manifest.file)].find((candidate) => existsSync(candidate));
586
+ if (!file) throw new Error(`the cover file ${manifest.file} is not in ${root}; render it again`);
587
+ const run = path.join(root, ".run", "cover");
588
+ const kept = (name) => (existsSync(path.join(run, name)) ? path.join(run, name) : undefined);
589
+ const treatment = JSON.parse(readFileSync(path.join(root, "treatment.json"), "utf8"));
590
+ return runCoverGate(file, { frames: kept("frames"), replay: kept("replay"), masks: kept("masks"), title: treatment.title?.box, strict: true });
591
+ }
592
+
572
593
  function main(argv) {
573
594
  if (!argv.length || argv.includes("--help")) {
574
- console.log("lit-typographic-motion gate\nUsage: node gate.mjs <output-dir> [--viewed N]\n node gate.mjs --done <output-dir> (0 DONE, 1 NOT DONE, 2 DONE_WITH_OPEN_ITEMS after round 3, 3 DONE_UNVIEWED)");
595
+ console.log("lit-typographic-motion gate\nUsage: node gate.mjs <output-dir> [--viewed N]\n node gate.mjs --done <output-dir> (0 DONE, 1 NOT DONE, 2 DONE_WITH_OPEN_ITEMS after round 3, 3 DONE_UNVIEWED)\n node gate.mjs --cover <cover.webp|cover.gif|frames-dir> [--frames d --replay d --masks d --title x,y,w,h --display-width N] [--strict] [--json]");
575
596
  return 0;
576
597
  }
577
598
  if (argv[0] === "--done") {
@@ -579,6 +600,18 @@ function main(argv) {
579
600
  console.log(formatDone(state));
580
601
  return state.code;
581
602
  }
603
+ const value = (name) => { const i = argv.indexOf(`--${name}`); return i >= 0 ? argv[i + 1] : undefined; };
604
+ if (argv[0] === "--cover") {
605
+ const gated = runCoverGate(path.resolve(argv[1]), { frames: value("frames"), replay: value("replay"), masks: value("masks"), title: value("title"), displayWidth: value("display-width") ? Number(value("display-width")) : undefined, strict: argv.includes("--strict") });
606
+ const { kept, steps, durations, ...shown } = gated.result;
607
+ process.stdout.write(argv.includes("--json") ? `${JSON.stringify(shown, null, 2)}\n` : gated.report);
608
+ return gated.exitCode;
609
+ }
610
+ if (existsSync(path.join(argv[0], "manifest.json")) && JSON.parse(readFileSync(path.join(argv[0], "manifest.json"), "utf8")).mode === "cover") {
611
+ const gated = coverDirGate(path.resolve(argv[0]));
612
+ process.stdout.write(gated.report);
613
+ return gated.exitCode;
614
+ }
582
615
  const viewedIndex = argv.indexOf("--viewed");
583
616
  const viewed = viewedIndex >= 0 ? Number(argv[viewedIndex + 1]) || 0 : undefined;
584
617
  const result = runGate(argv[0], { viewed });